├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── CunningDocumentScannerPlugin.h │ ├── CunningDocumentScannerPlugin.m │ ├── CunningScannerOptions.swift │ └── SwiftCunningDocumentScannerPlugin.swift ├── .gitignore └── cunning_document_scanner.podspec ├── android ├── settings.gradle ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ ├── integers.xml │ │ │ ├── themes.xml │ │ │ └── dimens.xml │ │ ├── xml │ │ │ └── file_paths.xml │ │ ├── drawable │ │ │ ├── ic_baseline_add_24.xml │ │ │ ├── ic_baseline_check_24.xml │ │ │ └── ic_baseline_arrow_back_24.xml │ │ ├── animator │ │ │ └── button_grow_animation.xml │ │ └── layout │ │ │ └── activity_image_crop.xml │ │ ├── kotlin │ │ └── biz │ │ │ └── cunning │ │ │ └── cunning_document_scanner │ │ │ ├── fallback │ │ │ ├── DocumentScannerFileProvider.kt │ │ │ ├── enums │ │ │ │ └── QuadCorner.kt │ │ │ ├── constants │ │ │ │ ├── DefaultSetting.kt │ │ │ │ └── DocumentScannerExtra.kt │ │ │ ├── models │ │ │ │ ├── Line.kt │ │ │ │ ├── Document.kt │ │ │ │ ├── Point.kt │ │ │ │ └── Quad.kt │ │ │ ├── extensions │ │ │ │ ├── ImageButton.kt │ │ │ │ ├── Bitmap.kt │ │ │ │ ├── AppCompatActivity.kt │ │ │ │ ├── Point.kt │ │ │ │ └── Canvas.kt │ │ │ ├── utils │ │ │ │ ├── FileUtil.kt │ │ │ │ ├── CameraUtil.kt │ │ │ │ └── ImageUtil.kt │ │ │ ├── ui │ │ │ │ ├── CircleButton.kt │ │ │ │ ├── DoneButton.kt │ │ │ │ └── ImageCropView.kt │ │ │ └── DocumentScannerActivity.kt │ │ │ └── CunningDocumentScannerPlugin.kt │ │ └── AndroidManifest.xml └── build.gradle ├── example ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── .gitignore │ ├── Podfile.lock │ └── Podfile ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle │ └── settings.gradle ├── .metadata ├── README.md ├── .gitignore ├── test │ └── widget_test.dart ├── analysis_options.yaml ├── lib │ └── main.dart ├── pubspec.yaml └── pubspec.lock ├── analysis_options.yaml ├── lib ├── cunning_document_scanner.dart └── src │ ├── ios_image_format.dart │ ├── exceptions.dart │ ├── ios_scanner_options.dart │ └── cunning_document_scanner.dart ├── .metadata ├── .github ├── workflows │ ├── publish.yml │ └── dart.yml └── dependabot.yml ├── .vscode └── launch.json ├── .gitignore ├── LICENSE ├── pubspec.yaml ├── test ├── mocks │ └── mock_permission_handler_platform.dart ├── cunning_document_scanner_test.dart └── src │ └── exceptions_test.dart ├── CHANGELOG.md └── README.md /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'cunning_document_scanner' 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Original Image With Cropper 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Classes/CunningDocumentScannerPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface CunningDocumentScannerPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /android/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #D0E4FF 5 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jachzen/cunning_document_scanner/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/res/values/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 100 4 | 150 5 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/DocumentScannerFileProvider.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback 2 | 3 | import androidx.core.content.FileProvider 4 | 5 | class DocumentScannerFileProvider: FileProvider() { 6 | } -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip 6 | -------------------------------------------------------------------------------- /lib/cunning_document_scanner.dart: -------------------------------------------------------------------------------- 1 | // Cunning Document Scanner - A simple document scanning library for Flutter. 2 | 3 | export 'src/cunning_document_scanner.dart'; 4 | export 'src/exceptions.dart'; 5 | export 'src/ios_image_format.dart'; 6 | export 'src/ios_scanner_options.dart'; 7 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/enums/QuadCorner.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.enums 2 | 3 | /** 4 | * enums for all 4 quad corners 5 | */ 6 | enum class QuadCorner { 7 | TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT 8 | } -------------------------------------------------------------------------------- /lib/src/ios_image_format.dart: -------------------------------------------------------------------------------- 1 | /// Enumerates the different output image formats are supported. 2 | enum IosImageFormat { 3 | /// Indicates the output image should be formatted as JPEG image. 4 | jpg, 5 | 6 | /// Indicates the output image should be formatted as PNG image. 7 | png, 8 | } 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/ic_baseline_add_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/ic_baseline_check_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to pub.dev 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | publish: 10 | permissions: 11 | id-token: write # Required for authentication using OIDC 12 | uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 13 | # with: 14 | # working-directory: path/to/package/within/repository -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/constants/DefaultSetting.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.constants 2 | 3 | /** 4 | * This class contains default document scanner options 5 | */ 6 | class DefaultSetting { 7 | companion object { 8 | const val CROPPED_IMAGE_QUALITY = 100 9 | const val MAX_NUM_DOCUMENTS = 24 10 | } 11 | } -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/ic_baseline_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "example", 9 | "cwd": "example", 10 | "request": "launch", 11 | "type": "dart" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/constants/DocumentScannerExtra.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.constants 2 | 3 | /** 4 | * This class contains constants meant to be used as intent extras 5 | */ 6 | class DocumentScannerExtra { 7 | companion object { 8 | const val EXTRA_CROPPED_IMAGE_QUALITY = "croppedImageQuality" 9 | const val EXTRA_MAX_NUM_DOCUMENTS = "maxNumDocuments" 10 | } 11 | } -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/models/Line.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.models 2 | 3 | import android.graphics.PointF 4 | 5 | /** 6 | * represents a line connecting 2 Android points 7 | * 8 | * @param fromPoint the 1st point 9 | * @param toPoint the 2nd point 10 | * @constructor creates a line connecting 2 points 11 | */ 12 | class Line(fromPoint: PointF, toPoint: PointF) { 13 | val from: PointF = fromPoint 14 | val to: PointF = toPoint 15 | } -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/ephemeral/ 38 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 25 | /pubspec.lock 26 | **/doc/api/ 27 | .dart_tool/ 28 | .packages 29 | build/ 30 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # cunning_document_scanner_example 2 | 3 | Demonstrates how to use the cunning_document_scanner plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/extensions/ImageButton.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.extensions 2 | 3 | import android.widget.ImageButton 4 | 5 | /** 6 | * This function adds an on click listener to the button. It makes the button not clickable, 7 | * calls the on click function, and then makes the button clickable. This prevents the on click 8 | * function from being called while it runs. 9 | * 10 | * @param onClick the click event handler 11 | */ 12 | fun ImageButton.onClick(onClick: () -> Unit) { 13 | setOnClickListener { 14 | isClickable = false 15 | onClick() 16 | isClickable = true 17 | } 18 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pub" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | time: "09:00" 13 | timezone: Europe/Berlin 14 | - package-ecosystem: "pub" 15 | directory: "/example/" 16 | schedule: 17 | interval: "weekly" 18 | time: "09:00" 19 | timezone: Europe/Berlin -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Flutter CI 7 | 8 | on: 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | - uses: subosito/flutter-action@v2 19 | 20 | - name: Install Dependencies 21 | run: flutter pub get 22 | 23 | - name: Analyze Project Source 24 | run: flutter analyze 25 | 26 | - name: Run tests 27 | run: flutter test 28 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Classes/CunningDocumentScannerPlugin.m: -------------------------------------------------------------------------------- 1 | #import "CunningDocumentScannerPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "cunning_document_scanner-Swift.h" 9 | #endif 10 | 11 | @implementation CunningDocumentScannerPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftCunningDocumentScannerPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/models/Document.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.models 2 | 3 | /** 4 | * This class contains the original document photo, and a cropper. The user can drag the corners 5 | * to make adjustments to the detected corners. 6 | * 7 | * @param originalPhotoFilePath the photo file path before cropping 8 | * @param originalPhotoWidth the original photo width 9 | * @param originalPhotoHeight the original photo height 10 | * @param corners the document's 4 corner points 11 | * @constructor creates a document 12 | */ 13 | class Document( 14 | val originalPhotoFilePath: String, 15 | private val originalPhotoWidth: Int, 16 | val originalPhotoHeight: Int, 17 | var corners: Quad 18 | ) { 19 | } -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version '8.13.1' apply false 23 | id "org.jetbrains.kotlin.android" version "2.2.21" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .cxx/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | -------------------------------------------------------------------------------- /ios/cunning_document_scanner.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint cunning_document_scanner.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'cunning_document_scanner' 7 | s.version = '1.0.0' 8 | s.summary = 'A new flutter plugin project.' 9 | s.description = <<-DESC 10 | A new flutter plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Cunning GmbH' => 'marcel@cunning.biz' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '13.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 22 | s.swift_version = '5.0' 23 | end 24 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:cunning_document_scanner_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('View is created', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => 22 | widget is Text && widget.data!.startsWith('Add Pictures'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/src/exceptions.dart: -------------------------------------------------------------------------------- 1 | /// Custom exceptions thrown by `CunningDocumentScanner`. 2 | class CunningDocumentScannerException implements Exception { 3 | /// A short message describing the error. 4 | final String message; 5 | 6 | /// Optional code to categorize errors (e.g. 'permission_denied'). 7 | final String? code; 8 | 9 | const CunningDocumentScannerException(this.message, {this.code}); 10 | 11 | /// Named constructor for permission errors. 12 | const CunningDocumentScannerException.permissionDenied([ 13 | String message = 'Permission not granted', 14 | ]) : this(message, code: 'permission_denied'); 15 | 16 | @override 17 | String toString() => 18 | 'CunningDocumentScannerException(${code ?? 'error'}): $message'; 19 | 20 | @override 21 | bool operator ==(Object other) => 22 | identical(this, other) || 23 | other is CunningDocumentScannerException && 24 | runtimeType == other.runtimeType && 25 | message == other.message && 26 | code == other.code; 27 | 28 | @override 29 | int get hashCode => message.hashCode ^ code.hashCode; 30 | } 31 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/extensions/Bitmap.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.extensions 2 | 3 | import android.graphics.Bitmap 4 | import java.io.File 5 | import java.io.FileOutputStream 6 | import kotlin.math.sqrt 7 | 8 | /** 9 | * This converts the bitmap to base64 10 | * 11 | * @param file the bitmap gets saved to this file 12 | */ 13 | fun Bitmap.saveToFile(file: File, quality: Int) { 14 | val fileOutputStream = FileOutputStream(file) 15 | compress(Bitmap.CompressFormat.JPEG, quality, fileOutputStream) 16 | fileOutputStream.close() 17 | } 18 | 19 | /** 20 | * This resizes the image, so that the byte count is a little less than targetBytes 21 | * 22 | * @param targetBytes the returned bitmap has a size a little less than targetBytes 23 | */ 24 | fun Bitmap.changeByteCountByResizing(targetBytes: Int): Bitmap { 25 | val scale = sqrt(targetBytes.toDouble() / byteCount.toDouble()) 26 | return Bitmap.createScaledBitmap( 27 | this, 28 | (width * scale).toInt(), 29 | (height * scale).toInt(), 30 | true 31 | ) 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marcel Pater 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/extensions/AppCompatActivity.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.extensions 2 | 3 | import android.graphics.Rect 4 | import androidx.appcompat.app.AppCompatActivity 5 | 6 | @Suppress("DEPRECATION") 7 | /** 8 | * @property screenBounds the screen bounds (used to get screen width and height) 9 | */ 10 | val AppCompatActivity.screenBounds: Rect get() { 11 | // currentWindowMetrics was added in Android R 12 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { 13 | return windowManager.currentWindowMetrics.bounds 14 | } 15 | 16 | // fall back to get screen width and height if using a version before Android R 17 | return Rect( 18 | 0, 0 , windowManager.defaultDisplay.width, windowManager.defaultDisplay.height 19 | ) 20 | } 21 | 22 | /** 23 | * @property screenWidth the screen width 24 | */ 25 | val AppCompatActivity.screenWidth: Int get() = screenBounds.width() 26 | 27 | /** 28 | * @property screenHeight the screen height 29 | */ 30 | val AppCompatActivity.screenHeight: Int get() = screenBounds.height() -------------------------------------------------------------------------------- /android/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0dp 4 | 1 5 | 0.1dp 6 | 5dp 7 | 200dp 8 | 25px 9 | 4px 10 | 5px 11 | 1dp 12 | 1.07 13 | 0dp 14 | 15.4dp 15 | 75dp 16 | 2.91dp 17 | 8dp 18 | 54.5dp 19 | 2dp 20 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'biz.cunning.cunning_document_scanner' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.8.22' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:8.3.0' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | namespace 'biz.cunning.cunning_document_scanner' 29 | 30 | compileSdk 34 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | minSdk 21 47 | } 48 | } 49 | 50 | dependencies { 51 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 52 | implementation 'com.google.android.gms:play-services-mlkit-document-scanner:16.0.0' 53 | } 54 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - cunning_document_scanner (1.0.0): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - path_provider_foundation (0.0.1): 6 | - Flutter 7 | - FlutterMacOS 8 | - permission_handler_apple (9.3.0): 9 | - Flutter 10 | 11 | DEPENDENCIES: 12 | - cunning_document_scanner (from `.symlinks/plugins/cunning_document_scanner/ios`) 13 | - Flutter (from `Flutter`) 14 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 15 | - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 16 | 17 | EXTERNAL SOURCES: 18 | cunning_document_scanner: 19 | :path: ".symlinks/plugins/cunning_document_scanner/ios" 20 | Flutter: 21 | :path: Flutter 22 | path_provider_foundation: 23 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 24 | permission_handler_apple: 25 | :path: ".symlinks/plugins/permission_handler_apple/ios" 26 | 27 | SPEC CHECKSUMS: 28 | cunning_document_scanner: 7cb9bd173f7cc7b11696dde98d01492187fc3a67 29 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 30 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 31 | permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 32 | 33 | PODFILE CHECKSUM: e78c989774f3b5b54daf69ce13097109fe4b0da3 34 | 35 | COCOAPODS: 1.15.2 36 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 14 | 15 | 20 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/utils/FileUtil.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.utils 2 | 3 | import android.os.Environment 4 | import androidx.activity.ComponentActivity 5 | import java.io.File 6 | import java.io.IOException 7 | import java.text.SimpleDateFormat 8 | import java.util.Locale 9 | import java.util.Date 10 | 11 | /** 12 | * This class contains a helper function creating temporary files 13 | * 14 | * @constructor creates file util 15 | */ 16 | class FileUtil { 17 | /** 18 | * create a temporary file 19 | * 20 | * @param activity the current activity 21 | * @param pageNumber the current document page number 22 | */ 23 | @Throws(IOException::class) 24 | fun createImageFile(activity: ComponentActivity, pageNumber: Int): File { 25 | // use current time to make file name more unique 26 | val dateTime: String = SimpleDateFormat( 27 | "yyyyMMdd_HHmmss", 28 | Locale.US 29 | ).format(Date()) 30 | 31 | // create file in pictures directory 32 | val storageDir: File? = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES) 33 | return File.createTempFile( 34 | "DOCUMENT_SCAN_${pageNumber}_${dateTime}", 35 | ".jpg", 36 | storageDir 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: cunning_document_scanner 2 | description: A document scanner plugin for flutter. Scan and crop automatically on iOS and Android. 3 | version: 2.0.0 4 | homepage: https://cunning.biz 5 | repository: https://github.com/jachzen/cunning_document_scanner 6 | 7 | environment: 8 | sdk: '>=3.0.0 <4.0.0' 9 | flutter: ">=2.5.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | permission_handler: ^12.0.1 15 | permission_handler_platform_interface: ^4.3.0 16 | plugin_platform_interface: ^2.1.8 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | flutter_lints: ">=3.0.1 <7.0.0" 22 | mockito: ^5.6.1 23 | 24 | # For information on the generic Dart part of this file, see the 25 | # following page: https://dart.dev/tools/pub/pubspec 26 | 27 | # The following section is specific to Flutter. 28 | flutter: 29 | # This section identifies this Flutter project as a plugin project. 30 | # The 'pluginClass' and Android 'package' identifiers should not ordinarily 31 | # be modified. They are used by the tooling to maintain consistency when 32 | # adding or updating assets for this project. 33 | plugin: 34 | platforms: 35 | android: 36 | package: biz.cunning.cunning_document_scanner 37 | pluginClass: CunningDocumentScannerPlugin 38 | ios: 39 | pluginClass: CunningDocumentScannerPlugin 40 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/models/Point.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.models 2 | 3 | 4 | //javadoc:Point_ 5 | class Point @JvmOverloads constructor(x: Double = 0.0, y: Double = 0.0) { 6 | var x = 0.0 7 | var y = 0.0 8 | 9 | init { 10 | this.x = x 11 | this.y = y 12 | } 13 | 14 | fun set(vals: DoubleArray?) { 15 | if (vals != null) { 16 | x = if (vals.size > 0) vals[0] else 0.0 17 | y = if (vals.size > 1) vals[1] else 0.0 18 | } else { 19 | x = 0.0 20 | y = 0.0 21 | } 22 | } 23 | 24 | 25 | override fun hashCode(): Int { 26 | val prime = 31 27 | var result = 1 28 | var temp: Long 29 | temp = java.lang.Double.doubleToLongBits(x) 30 | result = prime * result + (temp xor (temp ushr 32)).toInt() 31 | temp = java.lang.Double.doubleToLongBits(y) 32 | result = prime * result + (temp xor (temp ushr 32)).toInt() 33 | return result 34 | } 35 | 36 | override fun equals(other: Any?): Boolean { 37 | if (this === other) return true 38 | if (other !is Point) return false 39 | val it = other 40 | return x == it.x && y == it.y 41 | } 42 | 43 | override fun toString(): String { 44 | return "{$x, $y}" 45 | } 46 | } -------------------------------------------------------------------------------- /lib/src/ios_scanner_options.dart: -------------------------------------------------------------------------------- 1 | import 'ios_image_format.dart'; 2 | 3 | /// Different options that modify the behavior of the document scanner on iOS. 4 | /// 5 | /// The [imageFormat] specifies the format of the output image file. Available 6 | /// options are [IosImageFormat.jpeg] or [IosImageFormat.png]. Default value is 7 | /// [IosImageFormat.png]. 8 | /// 9 | /// If [imageFormat] is set to [IosImageFormat.jpeg] the [jpgCompressionQuality] 10 | /// can be used to control the quality of the resulting JPEG image. The value 11 | /// 0.0 represents the maximum compression (or lowest quality) while the value 12 | /// 1.0 represents the least compression (or best quality). Default value is 1.0. 13 | final class IosScannerOptions { 14 | /// Creates a [IosScannerOptions]. 15 | const IosScannerOptions({ 16 | this.imageFormat = IosImageFormat.png, 17 | this.jpgCompressionQuality = 1.0, 18 | }); 19 | 20 | final IosImageFormat imageFormat; 21 | 22 | /// The quality of the resulting JPEG image, expressed as a value from 0.0 to 23 | /// 1.0. 24 | /// 25 | /// The value 0.0 represents the maximum compression (or lowest quality) while 26 | /// the value 1.0 represents the least compression (or best quality). The 27 | /// [jpgCompressionQuality] only has an effect if the [imageFormat] is set to 28 | /// [IosImageFormat.jpeg] and is ignored otherwise. 29 | final double jpgCompressionQuality; 30 | } 31 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /test/mocks/mock_permission_handler_platform.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/mockito.dart'; 2 | import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart'; 3 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 4 | 5 | class MockPermissionHandlerPlatform extends Mock 6 | with MockPlatformInterfaceMixin 7 | implements PermissionHandlerPlatform { 8 | final PermissionStatus permissionStatus; 9 | final ServiceStatus serviceStatus; 10 | 11 | MockPermissionHandlerPlatform( 12 | {this.permissionStatus = PermissionStatus.granted, 13 | this.serviceStatus = ServiceStatus.enabled}); 14 | 15 | @override 16 | Future checkPermissionStatus(Permission permission) => 17 | Future.value(permissionStatus); 18 | 19 | @override 20 | Future checkServiceStatus(Permission permission) => 21 | Future.value(serviceStatus); 22 | 23 | @override 24 | Future openAppSettings() => Future.value(true); 25 | 26 | @override 27 | Future> requestPermissions( 28 | List permissions) { 29 | var permissionsMap = { 30 | Permission.camera: permissionStatus 31 | }; 32 | return Future.value(permissionsMap); 33 | } 34 | 35 | @override 36 | Future shouldShowRequestPermissionRationale(Permission? permission) { 37 | return super.noSuchMethod( 38 | Invocation.method( 39 | #shouldShowPermissionRationale, 40 | [permission], 41 | ), 42 | returnValue: Future.value(true), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/extensions/Point.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.extensions 2 | 3 | import android.graphics.PointF 4 | import biz.cunning.cunning_document_scanner.fallback.models.Point 5 | import kotlin.math.pow 6 | import kotlin.math.sqrt 7 | 8 | /** 9 | * converts an OpenCV point to Android point 10 | * 11 | * @return Android point 12 | */ 13 | fun Point.toPointF(): PointF { 14 | return PointF(x.toFloat(), y.toFloat()) 15 | } 16 | 17 | 18 | /** 19 | * offset the OpenCV point by (dx, dy) 20 | * 21 | * @param dx horizontal offset 22 | * @param dy vertical offset 23 | * @return the OpenCV point after moving it (dx, dy) 24 | */ 25 | fun Point.move(dx: Double, dy: Double): Point { 26 | return Point(x + dx, y + dy) 27 | } 28 | 29 | /** 30 | * multiply an Android point by magnitude 31 | * 32 | * @return Android point after multiplying by magnitude 33 | */ 34 | fun PointF.multiply(magnitude: Float): PointF { 35 | return PointF(magnitude * x, magnitude * y) 36 | } 37 | 38 | /** 39 | * offset the Android point by (dx, dy) 40 | * 41 | * @param dx horizontal offset 42 | * @param dy vertical offset 43 | * @return the Android point after moving it (dx, dy) 44 | */ 45 | fun PointF.move(dx: Float, dy: Float): PointF { 46 | return PointF(x + dx, y + dy) 47 | } 48 | 49 | /** 50 | * calculates the distance between 2 Android points 51 | * 52 | * @param point the 2nd Android point 53 | * @return the distance between this point and the 2nd point 54 | */ 55 | fun PointF.distance(point: PointF): Float { 56 | return sqrt((point.x - x).pow(2) + (point.y - y).pow(2)) 57 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/ui/CircleButton.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.ui 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.util.AttributeSet 8 | import androidx.appcompat.widget.AppCompatImageButton 9 | import biz.cunning.cunning_document_scanner.R 10 | 11 | /** 12 | * This class creates a circular done button by modifying an image button. This is used for the 13 | * add new document button and retake photo button. 14 | * 15 | * @param context image button context 16 | * @param attrs image button attributes 17 | * @constructor creates circle button 18 | */ 19 | class CircleButton( 20 | context: Context, 21 | attrs: AttributeSet 22 | ): AppCompatImageButton(context, attrs) { 23 | /** 24 | * @property ring the button's outer ring 25 | */ 26 | private val ring = Paint(Paint.ANTI_ALIAS_FLAG) 27 | 28 | init { 29 | // set outer ring style 30 | ring.color = Color.WHITE 31 | ring.style = Paint.Style.STROKE 32 | ring.strokeWidth = resources.getDimension(R.dimen.small_button_ring_thickness) 33 | } 34 | 35 | /** 36 | * This gets called repeatedly. We use it to draw the button 37 | * 38 | * @param canvas the image button canvas 39 | */ 40 | override fun onDraw(canvas: Canvas) { 41 | super.onDraw(canvas) 42 | 43 | // draw outer ring 44 | canvas.drawCircle( 45 | (width / 2).toFloat(), 46 | (height / 2).toFloat(), 47 | (width.toFloat() - ring.strokeWidth) / 2, 48 | ring 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Classes/CunningScannerOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScannerOptions.swift 3 | // cunning_document_scanner 4 | // 5 | // Created by Maurits van Beusekom on 15/10/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CunningScannerImageFormat: String { 11 | case jpg 12 | case png 13 | } 14 | 15 | struct CunningScannerOptions { 16 | let imageFormat: CunningScannerImageFormat 17 | let jpgCompressionQuality: Double 18 | 19 | init() { 20 | self.imageFormat = CunningScannerImageFormat.png 21 | self.jpgCompressionQuality = 1.0 22 | } 23 | 24 | init(imageFormat: CunningScannerImageFormat) { 25 | self.imageFormat = imageFormat 26 | self.jpgCompressionQuality = 1.0 27 | } 28 | 29 | init(imageFormat: CunningScannerImageFormat, jpgCompressionQuality: Double) { 30 | self.imageFormat = imageFormat 31 | self.jpgCompressionQuality = jpgCompressionQuality 32 | } 33 | 34 | static func fromArguments(args: Any?) -> CunningScannerOptions { 35 | if (args == nil) { 36 | return CunningScannerOptions() 37 | } 38 | 39 | let arguments = args as? Dictionary 40 | 41 | if arguments == nil || arguments!.keys.contains("iosScannerOptions") == false { 42 | return CunningScannerOptions() 43 | } 44 | 45 | let scannerOptionsDict = arguments!["iosScannerOptions"] as! Dictionary 46 | let imageFormat: String = (scannerOptionsDict["imageFormat"] as? String) ?? "png" 47 | let jpgCompressionQuality: Double = (scannerOptionsDict["jpgCompressionQuality"] as? Double) ?? 1.0 48 | 49 | return CunningScannerOptions(imageFormat: CunningScannerImageFormat(rawValue: imageFormat) ?? CunningScannerImageFormat.png, jpgCompressionQuality: jpgCompressionQuality) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /android/src/main/res/animator/button_grow_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 25 | 29 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:cunning_document_scanner/cunning_document_scanner.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | void main() { 8 | runApp(const MyApp()); 9 | } 10 | 11 | class MyApp extends StatefulWidget { 12 | const MyApp({Key? key}) : super(key: key); 13 | 14 | @override 15 | State createState() => _MyAppState(); 16 | } 17 | 18 | class _MyAppState extends State { 19 | List _pictures = []; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | initPlatformState(); 25 | } 26 | 27 | // Platform messages are asynchronous, so we initialize in an async method. 28 | Future initPlatformState() async {} 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return MaterialApp( 33 | home: Scaffold( 34 | appBar: AppBar( 35 | title: const Text('Plugin example app'), 36 | ), 37 | body: SingleChildScrollView( 38 | child: Column( 39 | children: [ 40 | ElevatedButton( 41 | onPressed: onPressed, child: const Text("Add Pictures")), 42 | for (var picture in _pictures) Image.file(File(picture)) 43 | ], 44 | )), 45 | ), 46 | ); 47 | } 48 | 49 | void onPressed() async { 50 | List pictures; 51 | try { 52 | pictures = await CunningDocumentScanner.getPictures( 53 | isGalleryImportAllowed: true, 54 | iosScannerOptions: IosScannerOptions( 55 | imageFormat: IosImageFormat.jpg, 56 | jpgCompressionQuality: 0.5, 57 | )) ?? 58 | []; 59 | if (!mounted) return; 60 | setState(() { 61 | _pictures = pictures; 62 | }); 63 | } catch (exception) { 64 | // Handle exception here 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Cunning Document Scanner 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | cunning_document_scanner_example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | NSCameraUsageDescription 28 | Access to camera is required for scanning documents 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | CADisableMinimumFrameDurationOnPhone 49 | 50 | UIApplicationSupportsIndirectInputEvents 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/src/cunning_document_scanner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:permission_handler/permission_handler.dart'; 5 | 6 | import 'exceptions.dart'; 7 | import 'ios_scanner_options.dart'; 8 | 9 | /// A class that provides a simple way to scan documents. 10 | class CunningDocumentScanner { 11 | /// The method channel used to interact with the native platform. 12 | static const MethodChannel _channel = 13 | MethodChannel('cunning_document_scanner'); 14 | 15 | /// Starts the document scanning process. 16 | /// 17 | /// This method will open the camera and allow the user to scan documents. 18 | /// 19 | /// [noOfPages] is the maximum number of pages that can be scanned. 20 | /// [isGalleryImportAllowed] is a flag that allows the user to import images from the gallery. 21 | /// [iosScannerOptions] is a set of options for the iOS scanner. 22 | /// 23 | /// Returns a list of paths to the scanned images, or null if the user cancels the operation. 24 | static Future?> getPictures({ 25 | int noOfPages = 100, 26 | bool isGalleryImportAllowed = false, 27 | IosScannerOptions? iosScannerOptions, 28 | }) async { 29 | Map statuses = await [ 30 | Permission.camera, 31 | ].request(); 32 | if (statuses.containsValue(PermissionStatus.denied) || 33 | statuses.containsValue(PermissionStatus.permanentlyDenied)) { 34 | throw const CunningDocumentScannerException.permissionDenied( 35 | 'Camera permission not granted'); 36 | } 37 | 38 | final List? pictures = await _channel.invokeMethod('getPictures', { 39 | 'noOfPages': noOfPages, 40 | 'isGalleryImportAllowed': isGalleryImportAllowed, 41 | if (iosScannerOptions != null) 42 | 'iosScannerOptions': { 43 | 'imageFormat': iosScannerOptions.imageFormat.name, 44 | 'jpgCompressionQuality': iosScannerOptions.jpgCompressionQuality, 45 | } 46 | }); 47 | return pictures?.map((e) => e as String).toList(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "biz.cunning.cunning_document_scanner_example" 27 | compileSdk 36 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | 38 | sourceSets { 39 | main.java.srcDirs += 'src/main/kotlin' 40 | } 41 | 42 | defaultConfig { 43 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 44 | applicationId "biz.cunning.cunning_document_scanner_example" 45 | minSdkVersion flutter.minSdkVersion 46 | targetSdkVersion flutter.targetSdkVersion 47 | versionCode flutterVersionCode.toInteger() 48 | versionName flutterVersionName 49 | } 50 | 51 | buildTypes { 52 | release { 53 | // TODO: Add your own signing config for the release build. 54 | // Signing with the debug keys for now, so `flutter run --release` works. 55 | signingConfig signingConfigs.debug 56 | } 57 | } 58 | } 59 | 60 | flutter { 61 | source '../..' 62 | } 63 | 64 | dependencies {} 65 | 66 | configurations.configureEach { 67 | resolutionStrategy { 68 | eachDependency { 69 | if ((requested.group == "org.jetbrains.kotlin") && (requested.name.startsWith("kotlin-stdlib"))) { 70 | useVersion("2.2.21") 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | target.build_configurations.each do |config| 41 | # You can enable the permissions needed here. For example to enable camera 42 | # permission, just remove the `#` character in front so it looks like this: 43 | # 44 | # ## dart: PermissionGroup.camera 45 | # 'PERMISSION_CAMERA=1' 46 | # 47 | # Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h 48 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 49 | '$(inherited)', 50 | 51 | ## dart: PermissionGroup.calendar 52 | # 'PERMISSION_EVENTS=1', 53 | 54 | ## dart: PermissionGroup.reminders 55 | # 'PERMISSION_REMINDERS=1', 56 | 57 | ## dart: PermissionGroup.contacts 58 | # 'PERMISSION_CONTACTS=1', 59 | 60 | ## dart: PermissionGroup.camera 61 | 'PERMISSION_CAMERA=1'] 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/ui/DoneButton.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.ui 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.util.AttributeSet 8 | import androidx.appcompat.widget.AppCompatImageButton 9 | import androidx.core.content.ContextCompat 10 | import biz.cunning.cunning_document_scanner.R 11 | import biz.cunning.cunning_document_scanner.fallback.extensions.drawCheck 12 | 13 | /** 14 | * This class creates a circular done button by modifying an image button. The user presses 15 | * this button once they finish cropping an image 16 | * 17 | * @param context image button context 18 | * @param attrs image button attributes 19 | * @constructor creates done button 20 | */ 21 | class DoneButton( 22 | context: Context, 23 | attrs: AttributeSet 24 | ): AppCompatImageButton(context, attrs) { 25 | /** 26 | * @property ring the button's outer ring 27 | */ 28 | private val ring = Paint(Paint.ANTI_ALIAS_FLAG) 29 | 30 | /** 31 | * @property circle the button's inner circle 32 | */ 33 | private val circle = Paint(Paint.ANTI_ALIAS_FLAG) 34 | 35 | init { 36 | // set outer ring style 37 | ring.color = Color.WHITE 38 | ring.style = Paint.Style.STROKE 39 | ring.strokeWidth = resources.getDimension(R.dimen.large_button_ring_thickness) 40 | 41 | // set inner circle style 42 | circle.color = ContextCompat.getColor(context, R.color.done_button_inner_circle_color) 43 | circle.style = Paint.Style.FILL 44 | } 45 | 46 | /** 47 | * This gets called repeatedly. We use it to draw the done button. 48 | * 49 | * @param canvas the image button canvas 50 | */ 51 | override fun onDraw(canvas: Canvas) { 52 | super.onDraw(canvas) 53 | 54 | // calculate button center point, outer ring radius, and inner circle radius 55 | val centerX = width.toFloat() / 2 56 | val centerY = height.toFloat() / 2 57 | val outerRadius = (width.toFloat() - ring.strokeWidth) / 2 58 | val innerRadius = outerRadius - resources.getDimension( 59 | R.dimen.large_button_outer_ring_offset 60 | ) 61 | 62 | // draw outer ring 63 | canvas.drawCircle(centerX, centerY, outerRadius, ring) 64 | 65 | // draw inner circle 66 | canvas.drawCircle(centerX, centerY, innerRadius, circle) 67 | 68 | // draw check icon since it gets covered by inner circle 69 | canvas.drawCheck(centerX, centerY, drawable) 70 | } 71 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | ### Breaking Changes 3 | * Reorganized library structure: all implementation files moved to `lib/src/` directory. 4 | * Renamed `ios_options.dart` to `ios_scanner_options.dart` for better clarity. 5 | * Separated `IosImageFormat` enum into its own file (`ios_image_format.dart`). 6 | 7 | ### Improvements 8 | * Added custom exception `CunningDocumentScannerException` with specific error codes. 9 | * Replaced generic `Exception` with `CunningDocumentScannerException.permissionDenied()` for better error handling. 10 | * Improved code organization with barrel exports - users only need a single import. 11 | * Added comprehensive unit tests for custom exceptions. 12 | * Enhanced equality operators for `CunningDocumentScannerException`. 13 | 14 | ### Migration Guide 15 | * No changes required for users - the public API remains the same with `import 'package:cunning_document_scanner/cunning_document_scanner.dart';` 16 | * If catching exceptions, update catch blocks to use `CunningDocumentScannerException` instead of generic `Exception`. 17 | 18 | ## 1.4.0 19 | ### General 20 | * Bumped `permission_handler` to `12.0.1`. 21 | * Updated the example app to use Kotlin `2.2.21`, Android Gradle Plugin `8.13.1`, and Gradle `8.13`. 22 | * Added detailed documentation comments to the `CunningDocumentScanner` class. 23 | ### Android 24 | * Upgraded `play-services-mlkit-document-scanner` to `16.0.0`. 25 | * Updated `compileSdk` to `34`. 26 | 27 | ## 1.3.1 28 | * Upgraded dependencies. 29 | 30 | ## 1.3.0 31 | * Allow users to configure the image output type on iOS (PNG or JPEG). 32 | 33 | ## 1.2.3 34 | * Fix iOS crash where Documentscanner is not available 35 | 36 | ## 1.2.2 37 | * Fix bitmap exception crash on Android (thanks to rosenberg_ptr) 38 | 39 | ## 1.2.1 40 | * Add fallback for Android devices < 1.7GB RAM 41 | 42 | ## 1.2.0 43 | * Use ML kit on Android 44 | * dropped nocrop support 45 | * image quality dropped 46 | 47 | ## 1.1.5 48 | * Nmed parameters 49 | * crop default is false 50 | * dependencies updated 51 | * min ios version 12 now 52 | 53 | ## 1.1.4 54 | * Fixed iOS permission issue in example 55 | * upgraded permission_handler 56 | 57 | ## 1.1.3 58 | * Fixed permanently denied permission issue 59 | * Merged crop option for android - Thanks Edwin 60 | 61 | ## 1.1.2 62 | * iOS return unique filenames 63 | 64 | ## 1.1.1 65 | * Updated android documentscanner library 66 | 67 | ## 1.1.0 68 | * Exchanged android documentscanner with https://github.com/WebsiteBeaver/android-document-scanner 69 | 70 | ## 1.0.4 71 | * Fixed conflicting requestcodes issue 72 | 73 | ## 1.0.3 74 | * Updated permission handler constraint to ^10 75 | * Android fixed nullsafe access issues 76 | 77 | ## 1.0.2 78 | * Cleanup code - added images to README.md 79 | 80 | ## 1.0.1 81 | 82 | * Fixed Playstore issue exported activity. Added documentation. 83 | 84 | ## 1.0.0 85 | 86 | * Android and iOs Documentscanner based on Visionkit and AndroidDocument https://github.com/mayuce/AndroidDocumentScanner 87 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/utils/CameraUtil.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.utils 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.provider.MediaStore 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.result.ActivityResult 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.core.content.FileProvider 11 | import java.io.File 12 | import java.io.IOException 13 | 14 | /** 15 | * This class contains a helper function for opening the camera. 16 | * 17 | * @param activity current activity 18 | * @param onPhotoCaptureSuccess gets called with photo file path when photo is ready 19 | * @param onCancelPhoto gets called when user cancels out of camera 20 | * @constructor creates camera util 21 | */ 22 | class CameraUtil( 23 | private val activity: ComponentActivity, 24 | private val onPhotoCaptureSuccess: (photoFilePath: String) -> Unit, 25 | private val onCancelPhoto: () -> Unit 26 | ) { 27 | /** 28 | * @property photoFilePath the photo file path 29 | */ 30 | private lateinit var photoFilePath: String 31 | 32 | /** 33 | * @property startForResult used to launch camera 34 | */ 35 | private val startForResult = activity.registerForActivityResult( 36 | ActivityResultContracts.StartActivityForResult() 37 | ) { result: ActivityResult -> 38 | when (result.resultCode) { 39 | Activity.RESULT_OK -> { 40 | // send back photo file path on capture success 41 | onPhotoCaptureSuccess(photoFilePath) 42 | } 43 | Activity.RESULT_CANCELED -> { 44 | // delete the photo since the user didn't finish taking the photo 45 | File(photoFilePath).delete() 46 | onCancelPhoto() 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * open the camera by launching an image capture intent 53 | * 54 | * @param pageNumber the current document page number 55 | */ 56 | @Throws(IOException::class) 57 | fun openCamera(pageNumber: Int) { 58 | // create intent to launch camera 59 | val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) 60 | 61 | // create new file for photo 62 | val photoFile: File = FileUtil().createImageFile(activity, pageNumber) 63 | 64 | // store the photo file path, and send it back once the photo is saved 65 | photoFilePath = photoFile.absolutePath 66 | 67 | // photo gets saved to this file path 68 | val photoURI: Uri = FileProvider.getUriForFile( 69 | activity, 70 | "${activity.packageName}.DocumentScannerFileProvider", 71 | photoFile 72 | ) 73 | takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) 74 | 75 | // open camera 76 | startForResult.launch(takePictureIntent) 77 | } 78 | } -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/cunning_document_scanner_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:cunning_document_scanner/cunning_document_scanner.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart'; 5 | 6 | import 'mocks/mock_permission_handler_platform.dart'; 7 | 8 | void main() { 9 | group('CunningDocumentScanner permission denied', () { 10 | setUp(() { 11 | PermissionHandlerPlatform.instance = MockPermissionHandlerPlatform( 12 | permissionStatus: PermissionStatus.denied); 13 | }); 14 | 15 | test('getPictures with denied permission', () async { 16 | expect( 17 | () async => await CunningDocumentScanner.getPictures(), 18 | throwsA(isA() 19 | .having((e) => e.code, 'code', 'permission_denied') 20 | .having( 21 | (e) => e.message, 'message', 'Camera permission not granted')), 22 | ); 23 | }); 24 | }); 25 | 26 | group('CunningDocumentScanner permission permanently denied', () { 27 | setUp(() { 28 | PermissionHandlerPlatform.instance = MockPermissionHandlerPlatform( 29 | permissionStatus: PermissionStatus.permanentlyDenied); 30 | }); 31 | 32 | test('getPictures with permanently denied permission', () async { 33 | expect( 34 | () async => await CunningDocumentScanner.getPictures(), 35 | throwsA(isA() 36 | .having((e) => e.code, 'code', 'permission_denied') 37 | .having( 38 | (e) => e.message, 'message', 'Camera permission not granted')), 39 | ); 40 | }); 41 | }); 42 | 43 | group('CunningDocumentScanner Plugin exceptions', () { 44 | setUp(() { 45 | TestWidgetsFlutterBinding.ensureInitialized(); 46 | PermissionHandlerPlatform.instance = MockPermissionHandlerPlatform(); 47 | }); 48 | 49 | test('getPictures with MissingPluginException', () async { 50 | expect(() async => await CunningDocumentScanner.getPictures(), 51 | throwsA(isA())); 52 | }); 53 | }); 54 | 55 | group('CunningDocumentScanner granted permission', () { 56 | setUp(() { 57 | TestWidgetsFlutterBinding.ensureInitialized(); 58 | PermissionHandlerPlatform.instance = MockPermissionHandlerPlatform(); 59 | }); 60 | 61 | void loadPlatformChannel(WidgetTester tester, List result) { 62 | MethodChannel channel = const MethodChannel('cunning_document_scanner'); 63 | 64 | tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( 65 | channel, 66 | (MethodCall methodCall) => Future.value(result), 67 | ); 68 | } 69 | 70 | testWidgets('getPictures empty result', (WidgetTester tester) async { 71 | final List emptyResult = []; 72 | loadPlatformChannel(tester, emptyResult); 73 | final result = await CunningDocumentScanner.getPictures(); 74 | expect(result, emptyResult); 75 | }); 76 | 77 | testWidgets('getPictures multiple result', (WidgetTester tester) async { 78 | final List fakeResult = ['fake_url1', 'fake_url2', 'fake_url3']; 79 | loadPlatformChannel(tester, fakeResult); 80 | final result = await CunningDocumentScanner.getPictures(); 81 | expect(result, fakeResult); 82 | }); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /android/src/main/res/layout/activity_image_crop.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 27 | 28 | 32 | 33 | 41 | 42 | 43 | 44 | 48 | 49 | 57 | 58 | 59 | 60 | 64 | 65 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: cunning_document_scanner_example 2 | description: Demonstrates how to use the cunning_document_scanner plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.15.1 <4.0.0" 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | path_provider: ^2.1.5 19 | flutter: 20 | sdk: flutter 21 | 22 | cunning_document_scanner: 23 | # When depending on this package from a real application you should use: 24 | # cunning_document_scanner: ^x.y.z 25 | # See https://dart.dev/tools/pub/dependencies#version-constraints 26 | # The example app is bundled with the plugin so we use a path dependency on 27 | # the parent directory to use the current plugin's version. 28 | path: ../ 29 | 30 | # The following adds the Cupertino Icons font to your application. 31 | # Use with the CupertinoIcons class for iOS style icons. 32 | cupertino_icons: ^1.0.8 33 | 34 | dev_dependencies: 35 | flutter_test: 36 | sdk: flutter 37 | 38 | # The "flutter_lints" package below contains a set of recommended lints to 39 | # encourage good coding practices. The lint set provided by the package is 40 | # activated in the `analysis_options.yaml` file located at the root of your 41 | # package. See that file for information about deactivating specific lint 42 | # rules and activating additional ones. 43 | flutter_lints: ^6.0.0 44 | 45 | # For information on the generic Dart part of this file, see the 46 | # following page: https://dart.dev/tools/pub/pubspec 47 | 48 | # The following section is specific to Flutter. 49 | flutter: 50 | 51 | # The following line ensures that the Material Icons font is 52 | # included with your application, so that you can use the icons in 53 | # the material Icons class. 54 | uses-material-design: true 55 | 56 | # To add assets to your application, add an assets section, like this: 57 | # assets: 58 | # - images/a_dot_burr.jpeg 59 | # - images/a_dot_ham.jpeg 60 | 61 | # An image asset can refer to one or more resolution-specific "variants", see 62 | # https://flutter.dev/assets-and-images/#resolution-aware. 63 | 64 | # For details regarding adding assets from package dependencies, see 65 | # https://flutter.dev/assets-and-images/#from-packages 66 | 67 | # To add custom fonts to your application, add a fonts section here, 68 | # in this "flutter" section. Each entry in this list should have a 69 | # "family" key with the font family name, and a "fonts" key with a 70 | # list giving the asset and other descriptors for the font. For 71 | # example: 72 | # fonts: 73 | # - family: Schyler 74 | # fonts: 75 | # - asset: fonts/Schyler-Regular.ttf 76 | # - asset: fonts/Schyler-Italic.ttf 77 | # style: italic 78 | # - family: Trajan Pro 79 | # fonts: 80 | # - asset: fonts/TrajanPro.ttf 81 | # - asset: fonts/TrajanPro_Bold.ttf 82 | # weight: 700 83 | # 84 | # For details regarding fonts from package dependencies, 85 | # see https://flutter.dev/custom-fonts/#from-packages 86 | -------------------------------------------------------------------------------- /ios/Classes/SwiftCunningDocumentScannerPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import Vision 4 | import VisionKit 5 | 6 | @available(iOS 13.0, *) 7 | public class SwiftCunningDocumentScannerPlugin: NSObject, FlutterPlugin, VNDocumentCameraViewControllerDelegate { 8 | var resultChannel: FlutterResult? 9 | var presentingController: VNDocumentCameraViewController? 10 | var scannerOptions: CunningScannerOptions = CunningScannerOptions() 11 | 12 | public static func register(with registrar: FlutterPluginRegistrar) { 13 | let channel = FlutterMethodChannel(name: "cunning_document_scanner", binaryMessenger: registrar.messenger()) 14 | let instance = SwiftCunningDocumentScannerPlugin() 15 | registrar.addMethodCallDelegate(instance, channel: channel) 16 | } 17 | 18 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 19 | if call.method == "getPictures" { 20 | scannerOptions = CunningScannerOptions.fromArguments(args: call.arguments) 21 | let presentedVC: UIViewController? = UIApplication.shared.keyWindow?.rootViewController 22 | self.resultChannel = result 23 | if VNDocumentCameraViewController.isSupported { 24 | self.presentingController = VNDocumentCameraViewController() 25 | self.presentingController!.delegate = self 26 | presentedVC?.present(self.presentingController!, animated: true) 27 | } else { 28 | result(FlutterError(code: "UNAVAILABLE", message: "Document camera is not available on this device", details: nil)) 29 | } 30 | } else { 31 | result(FlutterMethodNotImplemented) 32 | return 33 | } 34 | } 35 | 36 | 37 | func getDocumentsDirectory() -> URL { 38 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) 39 | let documentsDirectory = paths[0] 40 | return documentsDirectory 41 | } 42 | 43 | public func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) { 44 | let tempDirPath = self.getDocumentsDirectory() 45 | let currentDateTime = Date() 46 | let df = DateFormatter() 47 | df.dateFormat = "yyyyMMdd-HHmmss" 48 | let formattedDate = df.string(from: currentDateTime) 49 | var filenames: [String] = [] 50 | for i in 0 ..< scan.pageCount { 51 | let page = scan.imageOfPage(at: i) 52 | let url = tempDirPath.appendingPathComponent(formattedDate + "-\(i).\(scannerOptions.imageFormat.rawValue)") 53 | switch scannerOptions.imageFormat { 54 | case CunningScannerImageFormat.jpg: 55 | try? page.jpegData(compressionQuality: scannerOptions.jpgCompressionQuality)?.write(to: url) 56 | break 57 | case CunningScannerImageFormat.png: 58 | try? page.pngData()?.write(to: url) 59 | break 60 | } 61 | 62 | filenames.append(url.path) 63 | } 64 | resultChannel?(filenames) 65 | presentingController?.dismiss(animated: true) 66 | } 67 | 68 | public func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) { 69 | resultChannel?(nil) 70 | presentingController?.dismiss(animated: true) 71 | } 72 | 73 | public func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) { 74 | resultChannel?(FlutterError(code: "ERROR", message: error.localizedDescription, details: nil)) 75 | presentingController?.dismiss(animated: true) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/extensions/Canvas.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.extensions 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Matrix 5 | import android.graphics.Paint 6 | import android.graphics.PointF 7 | import android.graphics.RectF 8 | import android.graphics.drawable.Drawable 9 | import biz.cunning.cunning_document_scanner.fallback.enums.QuadCorner 10 | import biz.cunning.cunning_document_scanner.fallback.models.Line 11 | import biz.cunning.cunning_document_scanner.fallback.models.Quad 12 | 13 | /** 14 | * This draws a quad (used to draw cropper). It draws 4 circles and 15 | * 4 connecting lines 16 | * 17 | * @param quad 4 corners 18 | * @param pointRadius corner circle radius 19 | * @param cropperLinesAndCornersStyles quad style (color, thickness for example) 20 | * @param cropperSelectedCornerFillStyles style for selected corner 21 | * @param selectedCorner selected corner 22 | */ 23 | fun Canvas.drawQuad( 24 | quad: Quad, 25 | pointRadius: Float, 26 | cropperLinesAndCornersStyles: Paint, 27 | cropperSelectedCornerFillStyles: Paint, 28 | selectedCorner: QuadCorner?, 29 | imagePreviewBounds: RectF, 30 | ratio: Float, 31 | selectedCornerRadiusMagnification: Float, 32 | selectedCornerBackgroundMagnification: Float, 33 | ) { 34 | // draw 4 corner points 35 | for ((quadCorner: QuadCorner, cornerPoint: PointF) in quad.corners) { 36 | var circleRadius = pointRadius 37 | 38 | if (quadCorner === selectedCorner) { 39 | // the cropper corner circle grows when you touch and drag it 40 | circleRadius = selectedCornerRadiusMagnification * pointRadius 41 | val matrix = Matrix() 42 | matrix.postScale(ratio, ratio, ratio / cornerPoint.x, ratio /cornerPoint.y) 43 | matrix.postTranslate(imagePreviewBounds.left, imagePreviewBounds.top) 44 | matrix.postScale( 45 | selectedCornerBackgroundMagnification, 46 | selectedCornerBackgroundMagnification, 47 | cornerPoint.x, 48 | cornerPoint.y 49 | ) 50 | cropperSelectedCornerFillStyles.shader.setLocalMatrix(matrix) 51 | // fill selected corner circle with magnified image, so it's easier to crop 52 | drawCircle(cornerPoint.x, cornerPoint.y, circleRadius, cropperSelectedCornerFillStyles) 53 | } 54 | 55 | // draw corner circles 56 | drawCircle( 57 | cornerPoint.x, 58 | cornerPoint.y, 59 | circleRadius, 60 | cropperLinesAndCornersStyles 61 | ) 62 | } 63 | 64 | // draw 4 connecting lines 65 | for (edge: Line in quad.edges) { 66 | drawLine(edge.from.x, edge.from.y, edge.to.x, edge.to.y, cropperLinesAndCornersStyles) 67 | } 68 | } 69 | 70 | /** 71 | * This draws the check icon on the finish document scan button. It's needed 72 | * because the inner circle covers the check icon. 73 | * 74 | * @param buttonCenterX the button center x coordinate 75 | * @param buttonCenterY the button center y coordinate 76 | * @param drawable the check icon 77 | */ 78 | fun Canvas.drawCheck(buttonCenterX: Float, buttonCenterY: Float, drawable: Drawable) { 79 | val mutate = drawable.constantState?.newDrawable()?.mutate() 80 | mutate?.setBounds( 81 | (buttonCenterX - drawable.intrinsicWidth.toFloat() / 2).toInt(), 82 | (buttonCenterY - drawable.intrinsicHeight.toFloat() / 2).toInt(), 83 | (buttonCenterX + drawable.intrinsicWidth.toFloat() / 2).toInt(), 84 | (buttonCenterY + drawable.intrinsicHeight.toFloat() / 2).toInt() 85 | ) 86 | mutate?.draw(this) 87 | } -------------------------------------------------------------------------------- /test/src/exceptions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:cunning_document_scanner/cunning_document_scanner.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | group('CunningDocumentScannerException', () { 6 | test('creates exception with message and code', () { 7 | const exception = CunningDocumentScannerException( 8 | 'Test error', 9 | code: 'test_code', 10 | ); 11 | 12 | expect(exception.message, 'Test error'); 13 | expect(exception.code, 'test_code'); 14 | }); 15 | 16 | test('creates exception with message only', () { 17 | const exception = CunningDocumentScannerException('Test error'); 18 | 19 | expect(exception.message, 'Test error'); 20 | expect(exception.code, isNull); 21 | }); 22 | 23 | test('toString returns formatted error message with code', () { 24 | const exception = CunningDocumentScannerException( 25 | 'Test error', 26 | code: 'test_code', 27 | ); 28 | 29 | expect( 30 | exception.toString(), 31 | 'CunningDocumentScannerException(test_code): Test error', 32 | ); 33 | }); 34 | 35 | test('toString returns formatted error message without code', () { 36 | const exception = CunningDocumentScannerException('Test error'); 37 | 38 | expect( 39 | exception.toString(), 40 | 'CunningDocumentScannerException(error): Test error', 41 | ); 42 | }); 43 | 44 | group('permissionDenied constructor', () { 45 | test('creates exception with default message', () { 46 | const exception = CunningDocumentScannerException.permissionDenied(); 47 | 48 | expect(exception.message, 'Permission not granted'); 49 | expect(exception.code, 'permission_denied'); 50 | }); 51 | 52 | test('creates exception with custom message', () { 53 | const exception = CunningDocumentScannerException.permissionDenied( 54 | 'Camera permission denied', 55 | ); 56 | 57 | expect(exception.message, 'Camera permission denied'); 58 | expect(exception.code, 'permission_denied'); 59 | }); 60 | 61 | test('toString returns formatted permission error', () { 62 | const exception = CunningDocumentScannerException.permissionDenied( 63 | 'Camera permission denied', 64 | ); 65 | 66 | expect( 67 | exception.toString(), 68 | 'CunningDocumentScannerException(permission_denied): Camera permission denied', 69 | ); 70 | }); 71 | }); 72 | 73 | group('equality', () { 74 | test('two exceptions with same message and code are equal', () { 75 | const exception1 = CunningDocumentScannerException( 76 | 'Test error', 77 | code: 'test_code', 78 | ); 79 | const exception2 = CunningDocumentScannerException( 80 | 'Test error', 81 | code: 'test_code', 82 | ); 83 | 84 | expect(exception1, equals(exception2)); 85 | expect(exception1.hashCode, equals(exception2.hashCode)); 86 | }); 87 | 88 | test('two exceptions with different messages are not equal', () { 89 | const exception1 = CunningDocumentScannerException( 90 | 'Test error 1', 91 | code: 'test_code', 92 | ); 93 | const exception2 = CunningDocumentScannerException( 94 | 'Test error 2', 95 | code: 'test_code', 96 | ); 97 | 98 | expect(exception1, isNot(equals(exception2))); 99 | }); 100 | 101 | test('two exceptions with different codes are not equal', () { 102 | const exception1 = CunningDocumentScannerException( 103 | 'Test error', 104 | code: 'code1', 105 | ); 106 | const exception2 = CunningDocumentScannerException( 107 | 'Test error', 108 | code: 'code2', 109 | ); 110 | 111 | expect(exception1, isNot(equals(exception2))); 112 | }); 113 | 114 | test('exception equals itself', () { 115 | const exception = CunningDocumentScannerException( 116 | 'Test error', 117 | code: 'test_code', 118 | ); 119 | 120 | expect(exception, equals(exception)); 121 | }); 122 | }); 123 | 124 | group('implements Exception', () { 125 | test('can be caught as Exception', () { 126 | expect( 127 | () => throw const CunningDocumentScannerException('Test error'), 128 | throwsA(isA()), 129 | ); 130 | }); 131 | 132 | test('can be caught as CunningDocumentScannerException', () { 133 | expect( 134 | () => throw const CunningDocumentScannerException('Test error'), 135 | throwsA(isA()), 136 | ); 137 | }); 138 | }); 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/utils/ImageUtil.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.utils 2 | 3 | import android.content.ContentResolver 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.graphics.Canvas 7 | import android.graphics.Matrix 8 | import android.graphics.Paint 9 | import android.media.ExifInterface 10 | import android.net.Uri 11 | import biz.cunning.cunning_document_scanner.fallback.models.Quad 12 | import kotlin.math.pow 13 | import kotlin.math.sqrt 14 | 15 | 16 | class ImageUtil { 17 | 18 | fun getImageFromFilePath(filePath: String): Bitmap? { 19 | val rotation = getRotationDegrees(filePath) 20 | val bitmap = BitmapFactory.decodeFile(filePath) ?: return null 21 | 22 | return if (rotation != 0) { 23 | val matrix = Matrix().apply { postRotate(rotation.toFloat()) } 24 | Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) 25 | } else { 26 | bitmap 27 | } 28 | } 29 | 30 | private fun getRotationDegrees(filePath: String): Int { 31 | val exif = ExifInterface(filePath) 32 | return when (exif.getAttributeInt( 33 | ExifInterface.TAG_ORIENTATION, 34 | ExifInterface.ORIENTATION_NORMAL 35 | )) { 36 | ExifInterface.ORIENTATION_ROTATE_90 -> 90 37 | ExifInterface.ORIENTATION_ROTATE_180 -> 180 38 | ExifInterface.ORIENTATION_ROTATE_270 -> 270 39 | else -> 0 40 | } 41 | } 42 | 43 | 44 | fun crop(photoFilePath: String, corners: Quad): Bitmap? { 45 | val bitmap = getImageFromFilePath(photoFilePath) ?: return null 46 | 47 | // Convert Quad corners to a float array manually 48 | val src = floatArrayOf( 49 | corners.topLeftCorner.x, corners.topLeftCorner.y, 50 | corners.topRightCorner.x, corners.topRightCorner.y, 51 | corners.bottomRightCorner.x, corners.bottomRightCorner.y, 52 | corners.bottomLeftCorner.x, corners.bottomLeftCorner.y 53 | ) 54 | 55 | val avgWidth = getAvgWidth(corners) 56 | val avgHeight = getAvgHeight(corners) 57 | 58 | // Maintain the aspect ratio based on the longer dimension 59 | val aspectRatio = avgWidth / avgHeight 60 | 61 | val dstWidth: Float 62 | val dstHeight: Float 63 | 64 | if (aspectRatio >= 1) { // Width is greater than height, landscape orientation 65 | dstWidth = avgWidth 66 | dstHeight = dstWidth / aspectRatio 67 | } else { // Height is greater than width, portrait orientation 68 | dstHeight = avgHeight 69 | dstWidth = dstHeight * aspectRatio 70 | } 71 | 72 | // Use dstWidth and dstHeight to define your dst points accordingly 73 | val dst = floatArrayOf( 74 | 0f, 0f, // Top-left 75 | dstWidth, 0f, // Top-right 76 | dstWidth, dstHeight, // Bottom-right 77 | 0f, dstHeight // Bottom-left 78 | ) 79 | 80 | return correctPerspective(bitmap, src, dst, dstWidth, dstHeight) 81 | } 82 | 83 | fun correctPerspective(b: Bitmap, srcPoints: FloatArray?, dstPoints: FloatArray?, w: Float, h: Float): Bitmap { 84 | val result = Bitmap.createBitmap(w.toInt(), h.toInt(), Bitmap.Config.ARGB_8888) 85 | val p = Paint(Paint.ANTI_ALIAS_FLAG) 86 | val c = Canvas(result) 87 | val m = Matrix() 88 | m.setPolyToPoly(srcPoints, 0, dstPoints, 0, 4) 89 | c.drawBitmap(b, m, p) 90 | return result 91 | } 92 | 93 | private fun getAvgWidth(corners: Quad): Float { 94 | val widthTop = sqrt( 95 | (corners.topRightCorner.x - corners.topLeftCorner.x).toDouble().pow(2.0) + (corners.topRightCorner.y - corners.topLeftCorner.y).toDouble() 96 | .pow(2.0) 97 | ).toFloat() 98 | val widthBottom = sqrt( 99 | (corners.bottomLeftCorner.x - corners.bottomRightCorner.x).toDouble().pow(2.0) + (corners.bottomLeftCorner.y - corners.bottomRightCorner.y).toDouble() 100 | .pow(2.0) 101 | ).toFloat() 102 | return (widthTop + widthBottom) / 2 103 | } 104 | 105 | private fun getAvgHeight(corners: Quad): Float { 106 | val heightLeft = sqrt( 107 | (corners.bottomLeftCorner.x - corners.topLeftCorner.x).toDouble().pow(2.0) + (corners.bottomLeftCorner.y - corners.topLeftCorner.y).toDouble() 108 | .pow(2.0) 109 | ).toFloat() 110 | val heightRight = sqrt( 111 | (corners.topRightCorner.x - corners.bottomRightCorner.x).toDouble().pow(2.0) + (corners.topRightCorner.y - corners.bottomRightCorner.y).toDouble() 112 | .pow(2.0) 113 | ).toFloat() 114 | return (heightLeft + heightRight) / 2 115 | } 116 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/models/Quad.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.models 2 | 3 | import android.graphics.PointF 4 | import android.graphics.RectF 5 | import biz.cunning.cunning_document_scanner.fallback.enums.QuadCorner 6 | import biz.cunning.cunning_document_scanner.fallback.extensions.distance 7 | import biz.cunning.cunning_document_scanner.fallback.extensions.move 8 | import biz.cunning.cunning_document_scanner.fallback.extensions.multiply 9 | import biz.cunning.cunning_document_scanner.fallback.extensions.toPointF 10 | 11 | /** 12 | * This class is used to represent the cropper. It contains 4 corners. 13 | * 14 | * @param topLeftCorner the top left corner 15 | * @param topRightCorner the top right corner 16 | * @param bottomRightCorner the bottom right corner 17 | * @param bottomLeftCorner the bottom left corner 18 | * @constructor creates a quad from Android points 19 | */ 20 | class Quad( 21 | val topLeftCorner: PointF, 22 | val topRightCorner: PointF, 23 | val bottomRightCorner: PointF, 24 | val bottomLeftCorner: PointF 25 | ) { 26 | /** 27 | * @constructor creates a quad from OpenCV points 28 | */ 29 | constructor( 30 | topLeftCorner: Point, 31 | topRightCorner: Point, 32 | bottomRightCorner: Point, 33 | bottomLeftCorner: Point 34 | ) : this( 35 | topLeftCorner.toPointF(), 36 | topRightCorner.toPointF(), 37 | bottomRightCorner.toPointF(), 38 | bottomLeftCorner.toPointF() 39 | ) 40 | 41 | /** 42 | * @property corners lets us get the point coordinates for any corner 43 | */ 44 | var corners: MutableMap = mutableMapOf( 45 | QuadCorner.TOP_LEFT to topLeftCorner, 46 | QuadCorner.TOP_RIGHT to topRightCorner, 47 | QuadCorner.BOTTOM_RIGHT to bottomRightCorner, 48 | QuadCorner.BOTTOM_LEFT to bottomLeftCorner 49 | ) 50 | 51 | /** 52 | * @property edges 4 lines that connect the 4 corners 53 | */ 54 | val edges: Array get() = arrayOf( 55 | Line(topLeftCorner, topRightCorner), 56 | Line(topRightCorner, bottomRightCorner), 57 | Line(bottomRightCorner, bottomLeftCorner), 58 | Line(bottomLeftCorner, topLeftCorner) 59 | ) 60 | 61 | /** 62 | * This finds the corner that's closest to a point. When a user touches to drag 63 | * the cropper, that point is used to determine which corner to move. 64 | * 65 | * @param point we want to find the corner closest to this point 66 | * @return the closest corner 67 | */ 68 | fun getCornerClosestToPoint(point: PointF): QuadCorner { 69 | return corners.minByOrNull { corner -> corner.value.distance(point) }?.key!! 70 | } 71 | 72 | /** 73 | * This moves a corner by (dx, dy) 74 | * 75 | * @param corner the corner that needs to be moved 76 | * @param dx the corner moves dx horizontally 77 | * @param dy the corner moves dy vertically 78 | */ 79 | fun moveCorner(corner: QuadCorner, dx: Float, dy: Float) { 80 | corners[corner]?.offset(dx, dy) 81 | } 82 | 83 | /** 84 | * This maps original image coordinates to preview image coordinates. The original image 85 | * is probably larger than the preview image. 86 | * 87 | * @param imagePreviewBounds offset the point by the top left of imagePreviewBounds 88 | * @param ratio multiply the point by ratio 89 | * @return the 4 corners after mapping coordinates 90 | */ 91 | fun mapOriginalToPreviewImageCoordinates(imagePreviewBounds: RectF, ratio: Float): Quad { 92 | return Quad( 93 | topLeftCorner.multiply(ratio).move( 94 | imagePreviewBounds.left, 95 | imagePreviewBounds.top 96 | ), 97 | topRightCorner.multiply(ratio).move( 98 | imagePreviewBounds.left, 99 | imagePreviewBounds.top 100 | ), 101 | bottomRightCorner.multiply(ratio).move( 102 | imagePreviewBounds.left, 103 | imagePreviewBounds.top 104 | ), 105 | bottomLeftCorner.multiply(ratio).move( 106 | imagePreviewBounds.left, 107 | imagePreviewBounds.top 108 | ) 109 | ) 110 | } 111 | 112 | /** 113 | * This maps preview image coordinates to original image coordinates. The original image 114 | * is probably larger than the preview image. 115 | * 116 | * @param imagePreviewBounds reverse offset the point by the top left of imagePreviewBounds 117 | * @param ratio divide the point by ratio 118 | * @return the 4 corners after mapping coordinates 119 | */ 120 | fun mapPreviewToOriginalImageCoordinates(imagePreviewBounds: RectF, ratio: Float): Quad { 121 | return Quad( 122 | topLeftCorner.move( 123 | -imagePreviewBounds.left, 124 | -imagePreviewBounds.top 125 | ).multiply(1 / ratio), 126 | topRightCorner.move( 127 | -imagePreviewBounds.left, 128 | -imagePreviewBounds.top 129 | ).multiply(1 / ratio), 130 | bottomRightCorner.move( 131 | -imagePreviewBounds.left, 132 | -imagePreviewBounds.top 133 | ).multiply(1 / ratio), 134 | bottomLeftCorner.move( 135 | -imagePreviewBounds.left, 136 | -imagePreviewBounds.top 137 | ).multiply(1 / ratio) 138 | ) 139 | } 140 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cunning Document Scanner 2 | 3 | Cunning Document Scanner is a Flutter-based document scanner application that enables you to capture images of paper documents and convert them into digital files effortlessly. This application is designed to run on Android and iOS devices with minimum API levels of 21 and 13, respectively. 4 | 5 | ## Key Features 6 | 7 | - Fast and easy document scanning. 8 | - Conversion of document images into digital files. 9 | - Support for both Android and iOS platforms. 10 | - Minimum requirements: API 21 on Android, iOS 13 on iOS. 11 | - Limit the number of scannable files on Android. 12 | - Allows selection of images from the gallery on Android. 13 | 14 | A state of the art document scanner with automatic cropping function. 15 | 16 | 17 | 18 | 19 | 20 | ## Project Setup 21 | Follow the steps below to set up your Flutter project on Android and iOS. 22 | 23 | ### **Android** 24 | 25 | #### Minimum Version Configuration 26 | Ensure you meet the minimum version requirements to run the application on Android devices. 27 | In the `android/app/build.gradle` file, verify that `minSdkVersion` is at least 21: 28 | 29 | ```gradle 30 | android { 31 | ... 32 | defaultConfig { 33 | ... 34 | minSdkVersion 21 35 | ... 36 | } 37 | ... 38 | } 39 | ``` 40 | 41 | ### **IOS** 42 | #### Minimum Version Configuration 43 | Ensure you meet the minimum version requirements to run the application on iOS devices. 44 | In the `ios/Podfile` file, make sure the iOS platform version is at least 13.0: 45 | 46 | ```ruby 47 | platform :ios, '13.0' 48 | ``` 49 | #### Permission Configuration 50 | 1. Add a String property to the app's Info.plist file with the key [NSCameraUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nscamerausagedescription) and the value as the description for why your app needs camera access. 51 | 52 | NSCameraUsageDescription 53 | Camera Permission Description 54 | 55 | 2. The [permission_handler](https://pub.dev/packages/permission_handler) dependency used by cunning_document_scanner use [macros](https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h) to control whether a permission is enabled. Add the following to your `Podfile` file: 56 | 57 | ```ruby 58 | post_install do |installer| 59 | installer.pods_project.targets.each do |target| 60 | ... # Here are some configurations automatically generated by flutter 61 | 62 | # Start of the permission_handler configuration 63 | target.build_configurations.each do |config| 64 | 65 | # You can enable the permissions needed here. For example to enable camera 66 | # permission, just remove the `#` character in front so it looks like this: 67 | # 68 | # ## dart: PermissionGroup.camera 69 | # 'PERMISSION_CAMERA=1' 70 | # 71 | # Preprocessor definitions can be found at: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h 72 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 73 | '$(inherited)', 74 | 75 | ## dart: PermissionGroup.camera 76 | 'PERMISSION_CAMERA=1', 77 | ] 78 | 79 | end 80 | # End of the permission_handler configuration 81 | end 82 | end 83 | ``` 84 | 85 | ## How to use ? 86 | 87 | The easiest way to get a list of images is: 88 | 89 | ```dart 90 | final imagesPath = await CunningDocumentScanner.getPictures(); 91 | ``` 92 | ### Android Specific 93 | 94 | There are some features in Android that allow you to adjust the scanner that will be ignored in iOS: 95 | 96 | ```dart 97 | final imagesPath = await CunningDocumentScanner.getPictures( 98 | noOfPages: 1, // Limit the number of pages to 1 99 | isGalleryImportAllowed, // Allow the user to also pick an image from his gallery 100 | ); 101 | ``` 102 | 103 | ### iOS Specific 104 | 105 | On iOS it is possible to configure which image format should be used to save of the document scans. Available options are PNG (default) or JPEG. In certain situations the JPEG format could drastically reduce the file size of the final scan. If you choose to use JPEG you can also specify a compression quality, where 0.0 is highest compression (lowest quality) and 1.0 (default) is the lowest compression (highest quality). Example usage is: 106 | 107 | ```dart 108 | // Returns images in JPEG format with a compression quality of 50%. 109 | final imagesPath = await CunningDocumentScanner.getPictures( 110 | iosScannerOptions: IosScannerOptions( 111 | imageFormat: IosImageFormat.jpg, 112 | jpgCompressionQuality: 0.5, 113 | ), 114 | ); 115 | ``` 116 | 117 | ## Installation 118 | 119 | Before you begin, make sure you have Flutter and Dart installed on your system. You can follow the [Flutter installation guide](https://flutter.dev/docs/get-started/install) for more information. 120 | 121 | 1. Clone this repository: 122 | 123 | ```bash 124 | git clone https://github.com/jachzen/cunning_document_scanner.git 125 | ``` 126 | 127 | 2. Navigate to the project directory: 128 | 129 | ```bash 130 | cd cunning_document_scanner 131 | ``` 132 | 133 | 3. Install dependencies: 134 | 135 | ```bash 136 | flutter pub get 137 | ``` 138 | 139 | 4. Run the application: 140 | 141 | ```bash 142 | flutter run 143 | ``` 144 | 145 | ## Contributions 146 | 147 | Contributions are welcome. If you want to contribute to the development of Cunning Document Scanner, follow these steps: 148 | 149 | 1. Fork the repository. 150 | 2. Create a branch for your contribution: `git checkout -b your_feature` 151 | 3. Make your changes and commit: `git commit -m 'Add a new feature'` 152 | 4. Push the branch: `git push origin your_feature` 153 | 5. Open a pull request on GitHub. 154 | 155 | ## Issues and Support 156 | 157 | If you encounter any issues or have questions, please open an [issue](https://github.com/jachzen/cunning_document_scanner/issues). We're here to help. 158 | 159 | ## License 160 | 161 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/CunningDocumentScannerPlugin.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner 2 | 3 | import android.app.Activity 4 | import android.content.ActivityNotFoundException 5 | import android.content.Intent 6 | import android.content.IntentSender 7 | import androidx.core.app.ActivityCompat 8 | import biz.cunning.cunning_document_scanner.fallback.DocumentScannerActivity 9 | import biz.cunning.cunning_document_scanner.fallback.constants.DocumentScannerExtra 10 | import com.google.mlkit.common.MlKitException 11 | import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions 12 | import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.RESULT_FORMAT_JPEG 13 | import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.SCANNER_MODE_FULL 14 | import com.google.mlkit.vision.documentscanner.GmsDocumentScanning 15 | import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult 16 | import io.flutter.embedding.engine.plugins.FlutterPlugin 17 | import io.flutter.embedding.engine.plugins.activity.ActivityAware 18 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 19 | import io.flutter.plugin.common.MethodCall 20 | import io.flutter.plugin.common.MethodChannel 21 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 22 | import io.flutter.plugin.common.MethodChannel.Result 23 | import io.flutter.plugin.common.PluginRegistry 24 | 25 | 26 | /** CunningDocumentScannerPlugin */ 27 | class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { 28 | private var delegate: PluginRegistry.ActivityResultListener? = null 29 | private var binding: ActivityPluginBinding? = null 30 | private var pendingResult: Result? = null 31 | private lateinit var activity: Activity 32 | private val START_DOCUMENT_ACTIVITY: Int = 0x362738 33 | private val START_DOCUMENT_FB_ACTIVITY: Int = 0x362737 34 | 35 | 36 | /// The MethodChannel that will the communication between Flutter and native Android 37 | /// 38 | /// This local reference serves to register the plugin with the Flutter Engine and unregister it 39 | /// when the Flutter Engine is detached from the Activity 40 | private lateinit var channel: MethodChannel 41 | 42 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 43 | channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cunning_document_scanner") 44 | channel.setMethodCallHandler(this) 45 | } 46 | 47 | override fun onMethodCall(call: MethodCall, result: Result) { 48 | if (call.method == "getPictures") { 49 | val noOfPages = call.argument("noOfPages") ?: 50; 50 | val isGalleryImportAllowed = call.argument("isGalleryImportAllowed") ?: false; 51 | this.pendingResult = result 52 | startScan(noOfPages, isGalleryImportAllowed) 53 | } else { 54 | result.notImplemented() 55 | } 56 | } 57 | 58 | 59 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 60 | channel.setMethodCallHandler(null) 61 | } 62 | 63 | override fun onAttachedToActivity(binding: ActivityPluginBinding) { 64 | this.activity = binding.activity 65 | 66 | addActivityResultListener(binding) 67 | } 68 | 69 | private fun addActivityResultListener(binding: ActivityPluginBinding) { 70 | this.binding = binding 71 | if (this.delegate == null) { 72 | this.delegate = PluginRegistry.ActivityResultListener { requestCode, resultCode, data -> 73 | if (requestCode != START_DOCUMENT_ACTIVITY && requestCode != START_DOCUMENT_FB_ACTIVITY) { 74 | return@ActivityResultListener false 75 | } 76 | var handled = false 77 | if (requestCode == START_DOCUMENT_ACTIVITY) { 78 | when (resultCode) { 79 | Activity.RESULT_OK -> { 80 | // check for errors 81 | val error = data?.extras?.getString("error") 82 | if (error != null) { 83 | pendingResult?.error("ERROR", "error - $error", null) 84 | } else { 85 | // get an array with scanned document file paths 86 | val scanningResult: GmsDocumentScanningResult = 87 | data?.extras?.getParcelable("extra_scanning_result") 88 | ?: return@ActivityResultListener false 89 | 90 | val successResponse = scanningResult.pages?.map { 91 | it.imageUri.toString().removePrefix("file://") 92 | }?.toList() 93 | // trigger the success event handler with an array of cropped images 94 | pendingResult?.success(successResponse) 95 | } 96 | handled = true 97 | } 98 | 99 | Activity.RESULT_CANCELED -> { 100 | // user closed camera 101 | pendingResult?.success(emptyList()) 102 | handled = true 103 | } 104 | } 105 | } else { 106 | when (resultCode) { 107 | Activity.RESULT_OK -> { 108 | // check for errors 109 | val error = data?.extras?.getString("error") 110 | if (error != null) { 111 | pendingResult?.error("ERROR", "error - $error", null) 112 | } else { 113 | // get an array with scanned document file paths 114 | val croppedImageResults = 115 | data?.getStringArrayListExtra("croppedImageResults")?.toList() 116 | ?: let { 117 | pendingResult?.error("ERROR", "No cropped images returned", null) 118 | return@ActivityResultListener true 119 | } 120 | 121 | // return a list of file paths 122 | // removing file uri prefix as Flutter file will have problems with it 123 | val successResponse = croppedImageResults.map { 124 | it.removePrefix("file://") 125 | }.toList() 126 | // trigger the success event handler with an array of cropped images 127 | pendingResult?.success(successResponse) 128 | } 129 | handled = true 130 | } 131 | 132 | Activity.RESULT_CANCELED -> { 133 | // user closed camera 134 | pendingResult?.success(emptyList()) 135 | handled = true 136 | } 137 | } 138 | } 139 | 140 | if (handled) { 141 | // Clear the pending result to avoid reuse 142 | pendingResult = null 143 | } 144 | return@ActivityResultListener handled 145 | } 146 | } else { 147 | binding.removeActivityResultListener(this.delegate!!) 148 | } 149 | 150 | binding.addActivityResultListener(delegate!!) 151 | } 152 | 153 | 154 | /** 155 | * create intent to launch document scanner and set custom options 156 | */ 157 | private fun createDocumentScanIntent(noOfPages: Int): Intent { 158 | val documentScanIntent = Intent(activity, DocumentScannerActivity::class.java) 159 | 160 | documentScanIntent.putExtra( 161 | DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS, 162 | noOfPages 163 | ) 164 | 165 | return documentScanIntent 166 | } 167 | 168 | 169 | /** 170 | * add document scanner result handler and launch the document scanner 171 | */ 172 | private fun startScan(noOfPages: Int, isGalleryImportAllowed: Boolean) { 173 | val options = GmsDocumentScannerOptions.Builder() 174 | .setGalleryImportAllowed(isGalleryImportAllowed) 175 | .setPageLimit(noOfPages) 176 | .setResultFormats(RESULT_FORMAT_JPEG) 177 | .setScannerMode(SCANNER_MODE_FULL) 178 | .build() 179 | val scanner = GmsDocumentScanning.getClient(options) 180 | scanner.getStartScanIntent(activity).addOnSuccessListener { 181 | try { 182 | // Use a custom request code for onActivityResult identification 183 | activity.startIntentSenderForResult(it, START_DOCUMENT_ACTIVITY, null, 0, 0, 0) 184 | 185 | } catch (e: IntentSender.SendIntentException) { 186 | pendingResult?.error("ERROR", "Failed to start document scanner", null) 187 | } 188 | }.addOnFailureListener { 189 | if (it is MlKitException) { 190 | val intent = createDocumentScanIntent(noOfPages) 191 | try { 192 | ActivityCompat.startActivityForResult( 193 | this.activity, 194 | intent, 195 | START_DOCUMENT_FB_ACTIVITY, 196 | null 197 | ) 198 | } catch (e: ActivityNotFoundException) { 199 | pendingResult?.error("ERROR", "FAILED TO START ACTIVITY", null) 200 | } 201 | } else { 202 | pendingResult?.error("ERROR", "Failed to start document scanner Intent", null) 203 | } 204 | } 205 | } 206 | 207 | override fun onDetachedFromActivityForConfigChanges() { 208 | 209 | } 210 | 211 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { 212 | addActivityResultListener(binding) 213 | } 214 | 215 | override fun onDetachedFromActivity() { 216 | removeActivityResultListener() 217 | } 218 | 219 | private fun removeActivityResultListener() { 220 | this.delegate?.let { this.binding?.removeActivityResultListener(it) } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.13.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.2" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.4.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.2" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.19.1" 44 | cunning_document_scanner: 45 | dependency: "direct main" 46 | description: 47 | path: ".." 48 | relative: true 49 | source: path 50 | version: "2.0.0" 51 | cupertino_icons: 52 | dependency: "direct main" 53 | description: 54 | name: cupertino_icons 55 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 56 | url: "https://pub.dev" 57 | source: hosted 58 | version: "1.0.8" 59 | fake_async: 60 | dependency: transitive 61 | description: 62 | name: fake_async 63 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 64 | url: "https://pub.dev" 65 | source: hosted 66 | version: "1.3.3" 67 | ffi: 68 | dependency: transitive 69 | description: 70 | name: ffi 71 | sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" 72 | url: "https://pub.dev" 73 | source: hosted 74 | version: "2.1.4" 75 | flutter: 76 | dependency: "direct main" 77 | description: flutter 78 | source: sdk 79 | version: "0.0.0" 80 | flutter_lints: 81 | dependency: "direct dev" 82 | description: 83 | name: flutter_lints 84 | sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" 85 | url: "https://pub.dev" 86 | source: hosted 87 | version: "6.0.0" 88 | flutter_test: 89 | dependency: "direct dev" 90 | description: flutter 91 | source: sdk 92 | version: "0.0.0" 93 | flutter_web_plugins: 94 | dependency: transitive 95 | description: flutter 96 | source: sdk 97 | version: "0.0.0" 98 | leak_tracker: 99 | dependency: transitive 100 | description: 101 | name: leak_tracker 102 | sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" 103 | url: "https://pub.dev" 104 | source: hosted 105 | version: "11.0.2" 106 | leak_tracker_flutter_testing: 107 | dependency: transitive 108 | description: 109 | name: leak_tracker_flutter_testing 110 | sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" 111 | url: "https://pub.dev" 112 | source: hosted 113 | version: "3.0.10" 114 | leak_tracker_testing: 115 | dependency: transitive 116 | description: 117 | name: leak_tracker_testing 118 | sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" 119 | url: "https://pub.dev" 120 | source: hosted 121 | version: "3.0.2" 122 | lints: 123 | dependency: transitive 124 | description: 125 | name: lints 126 | sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 127 | url: "https://pub.dev" 128 | source: hosted 129 | version: "6.0.0" 130 | matcher: 131 | dependency: transitive 132 | description: 133 | name: matcher 134 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 135 | url: "https://pub.dev" 136 | source: hosted 137 | version: "0.12.17" 138 | material_color_utilities: 139 | dependency: transitive 140 | description: 141 | name: material_color_utilities 142 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 143 | url: "https://pub.dev" 144 | source: hosted 145 | version: "0.11.1" 146 | meta: 147 | dependency: transitive 148 | description: 149 | name: meta 150 | sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" 151 | url: "https://pub.dev" 152 | source: hosted 153 | version: "1.17.0" 154 | path: 155 | dependency: transitive 156 | description: 157 | name: path 158 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "1.9.1" 162 | path_provider: 163 | dependency: "direct main" 164 | description: 165 | name: path_provider 166 | sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "2.1.5" 170 | path_provider_android: 171 | dependency: transitive 172 | description: 173 | name: path_provider_android 174 | sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "2.2.22" 178 | path_provider_foundation: 179 | dependency: transitive 180 | description: 181 | name: path_provider_foundation 182 | sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "2.5.1" 186 | path_provider_linux: 187 | dependency: transitive 188 | description: 189 | name: path_provider_linux 190 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "2.2.1" 194 | path_provider_platform_interface: 195 | dependency: transitive 196 | description: 197 | name: path_provider_platform_interface 198 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 199 | url: "https://pub.dev" 200 | source: hosted 201 | version: "2.1.2" 202 | path_provider_windows: 203 | dependency: transitive 204 | description: 205 | name: path_provider_windows 206 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 207 | url: "https://pub.dev" 208 | source: hosted 209 | version: "2.3.0" 210 | permission_handler: 211 | dependency: transitive 212 | description: 213 | name: permission_handler 214 | sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 215 | url: "https://pub.dev" 216 | source: hosted 217 | version: "12.0.1" 218 | permission_handler_android: 219 | dependency: transitive 220 | description: 221 | name: permission_handler_android 222 | sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" 223 | url: "https://pub.dev" 224 | source: hosted 225 | version: "13.0.1" 226 | permission_handler_apple: 227 | dependency: transitive 228 | description: 229 | name: permission_handler_apple 230 | sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 231 | url: "https://pub.dev" 232 | source: hosted 233 | version: "9.4.7" 234 | permission_handler_html: 235 | dependency: transitive 236 | description: 237 | name: permission_handler_html 238 | sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" 239 | url: "https://pub.dev" 240 | source: hosted 241 | version: "0.1.3+5" 242 | permission_handler_platform_interface: 243 | dependency: transitive 244 | description: 245 | name: permission_handler_platform_interface 246 | sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 247 | url: "https://pub.dev" 248 | source: hosted 249 | version: "4.3.0" 250 | permission_handler_windows: 251 | dependency: transitive 252 | description: 253 | name: permission_handler_windows 254 | sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" 255 | url: "https://pub.dev" 256 | source: hosted 257 | version: "0.2.1" 258 | platform: 259 | dependency: transitive 260 | description: 261 | name: platform 262 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 263 | url: "https://pub.dev" 264 | source: hosted 265 | version: "3.1.6" 266 | plugin_platform_interface: 267 | dependency: transitive 268 | description: 269 | name: plugin_platform_interface 270 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 271 | url: "https://pub.dev" 272 | source: hosted 273 | version: "2.1.8" 274 | sky_engine: 275 | dependency: transitive 276 | description: flutter 277 | source: sdk 278 | version: "0.0.0" 279 | source_span: 280 | dependency: transitive 281 | description: 282 | name: source_span 283 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 284 | url: "https://pub.dev" 285 | source: hosted 286 | version: "1.10.1" 287 | stack_trace: 288 | dependency: transitive 289 | description: 290 | name: stack_trace 291 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 292 | url: "https://pub.dev" 293 | source: hosted 294 | version: "1.12.1" 295 | stream_channel: 296 | dependency: transitive 297 | description: 298 | name: stream_channel 299 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 300 | url: "https://pub.dev" 301 | source: hosted 302 | version: "2.1.4" 303 | string_scanner: 304 | dependency: transitive 305 | description: 306 | name: string_scanner 307 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 308 | url: "https://pub.dev" 309 | source: hosted 310 | version: "1.4.1" 311 | term_glyph: 312 | dependency: transitive 313 | description: 314 | name: term_glyph 315 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 316 | url: "https://pub.dev" 317 | source: hosted 318 | version: "1.2.2" 319 | test_api: 320 | dependency: transitive 321 | description: 322 | name: test_api 323 | sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 324 | url: "https://pub.dev" 325 | source: hosted 326 | version: "0.7.7" 327 | vector_math: 328 | dependency: transitive 329 | description: 330 | name: vector_math 331 | sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b 332 | url: "https://pub.dev" 333 | source: hosted 334 | version: "2.2.0" 335 | vm_service: 336 | dependency: transitive 337 | description: 338 | name: vm_service 339 | sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" 340 | url: "https://pub.dev" 341 | source: hosted 342 | version: "15.0.2" 343 | web: 344 | dependency: transitive 345 | description: 346 | name: web 347 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 348 | url: "https://pub.dev" 349 | source: hosted 350 | version: "1.1.1" 351 | xdg_directories: 352 | dependency: transitive 353 | description: 354 | name: xdg_directories 355 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 356 | url: "https://pub.dev" 357 | source: hosted 358 | version: "1.1.0" 359 | sdks: 360 | dart: ">=3.9.0 <4.0.0" 361 | flutter: ">=3.35.0" 362 | -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/ui/ImageCropView.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Bitmap 6 | import android.graphics.BitmapShader 7 | import android.graphics.Canvas 8 | import android.graphics.Color 9 | import android.graphics.Paint 10 | import android.graphics.PointF 11 | import android.graphics.RectF 12 | import android.graphics.Shader 13 | import android.util.AttributeSet 14 | import android.view.MotionEvent 15 | import androidx.appcompat.widget.AppCompatImageView 16 | import androidx.core.graphics.drawable.toBitmap 17 | import biz.cunning.cunning_document_scanner.R 18 | import biz.cunning.cunning_document_scanner.fallback.enums.QuadCorner 19 | import biz.cunning.cunning_document_scanner.fallback.extensions.changeByteCountByResizing 20 | import biz.cunning.cunning_document_scanner.fallback.extensions.drawQuad 21 | import biz.cunning.cunning_document_scanner.fallback.models.Quad 22 | 23 | /** 24 | * This class contains the original document photo, and a cropper. The user can drag the corners 25 | * to make adjustments to the detected corners. 26 | * 27 | * @param context view context 28 | * @param attrs view attributes 29 | * @constructor creates image crop view 30 | */ 31 | class ImageCropView(context: Context, attrs: AttributeSet) : AppCompatImageView(context, attrs) { 32 | 33 | /** 34 | * @property quad the 4 document corners 35 | */ 36 | private var quad: Quad? = null 37 | 38 | /** 39 | * @property prevTouchPoint keep track of where the user touches, so we know how much 40 | * to move corners on drag 41 | */ 42 | private var prevTouchPoint: PointF? = null 43 | 44 | /** 45 | * @property closestCornerToTouch if the user touches close to the top left corner for 46 | * example, that corner should move on drag 47 | */ 48 | private var closestCornerToTouch: QuadCorner? = null 49 | 50 | /** 51 | * @property cropperLinesAndCornersStyles paint style for 4 corners and connecting lines 52 | */ 53 | private val cropperLinesAndCornersStyles = Paint(Paint.ANTI_ALIAS_FLAG) 54 | 55 | /** 56 | * @property cropperSelectedCornerFillStyles when you tap and drag a cropper corner the circle 57 | * acts like a magnifying glass 58 | */ 59 | private val cropperSelectedCornerFillStyles = Paint() 60 | 61 | /** 62 | * @property imagePreviewHeight this is needed because height doesn't update immediately 63 | * after we set the image 64 | */ 65 | private var imagePreviewHeight = height 66 | 67 | /** 68 | * @property imagePreviewWidth this is needed because width doesn't update immediately 69 | * after we set the image 70 | */ 71 | private var imagePreviewWidth = width 72 | 73 | /** 74 | * @property ratio image container height to image height ratio used to map container 75 | * to image coordinates and vice versa 76 | */ 77 | private val ratio: Float get() = imagePreviewBounds.height() / drawable.intrinsicHeight 78 | 79 | /** 80 | * @property corners document corners in image preview coordinates 81 | */ 82 | val corners: Quad get() = quad!! 83 | 84 | /** 85 | * @property imagePreviewMaxSizeInBytes if the photo is too big, we need to scale it down 86 | * before we display it 87 | */ 88 | private val imagePreviewMaxSizeInBytes = 100 * 1024 * 1024 89 | 90 | init { 91 | // set cropper style 92 | cropperLinesAndCornersStyles.color = Color.WHITE 93 | cropperLinesAndCornersStyles.style = Paint.Style.STROKE 94 | cropperLinesAndCornersStyles.strokeWidth = 3f 95 | } 96 | 97 | /** 98 | * Initially the image preview height is 0. This calculates the height by using the photo 99 | * dimensions. It maintains the photo aspect ratio (we likely need to scale the photo down 100 | * to fit the preview container). 101 | * 102 | * @param photo the original photo with a rectangular document 103 | * @param screenWidth the device width 104 | */ 105 | fun setImagePreviewBounds(photo: Bitmap, screenWidth: Int, screenHeight: Int) { 106 | // image width to height aspect ratio 107 | val imageRatio = photo.width.toFloat() / photo.height.toFloat() 108 | val buttonsViewMinHeight = context.resources.getDimension( 109 | R.dimen.buttons_container_min_height 110 | ).toInt() 111 | 112 | imagePreviewHeight = if (photo.height > photo.width) { 113 | // if user takes the photo in portrait 114 | (screenWidth.toFloat() / imageRatio).toInt() 115 | } else { 116 | // if user takes the photo in landscape 117 | (screenWidth.toFloat() * imageRatio).toInt() 118 | } 119 | 120 | // set a cap on imagePreviewHeight, so that the bottom buttons container isn't hidden 121 | imagePreviewHeight = Integer.min( 122 | imagePreviewHeight, 123 | screenHeight - buttonsViewMinHeight 124 | ) 125 | 126 | imagePreviewWidth = screenWidth 127 | 128 | // image container initially has a 0 width and 0 height, calculate both and set them 129 | layoutParams.height = imagePreviewHeight 130 | layoutParams.width = imagePreviewWidth 131 | 132 | // refresh layout after we change height 133 | requestLayout() 134 | } 135 | 136 | /** 137 | * Insert bitmap in image view, and trigger onSetImage event handler 138 | */ 139 | fun setImage(photo: Bitmap) { 140 | var previewImagePhoto = photo 141 | // if the image is too large, we need to scale it down before displaying it 142 | if (photo.byteCount > imagePreviewMaxSizeInBytes) { 143 | previewImagePhoto = photo.changeByteCountByResizing(imagePreviewMaxSizeInBytes) 144 | } 145 | this.setImageBitmap(previewImagePhoto) 146 | this.onSetImage() 147 | } 148 | 149 | /** 150 | * Once the user takes a photo, we try to detect corners. This function stores them as quad. 151 | * 152 | * @param cropperCorners 4 corner points in original photo coordinates 153 | */ 154 | fun setCropper(cropperCorners: Quad) { 155 | quad = cropperCorners 156 | } 157 | 158 | /** 159 | * @property imagePreviewBounds image coordinates - if the image ratio is different than 160 | * the image container ratio then there's blank space either at the top and bottom of the 161 | * image or the left and right of the image 162 | */ 163 | val imagePreviewBounds: RectF 164 | get() { 165 | // image container width to height ratio 166 | val imageViewRatio: Float = imagePreviewWidth.toFloat() / imagePreviewHeight.toFloat() 167 | 168 | // image width to height ratio 169 | val imageRatio = drawable.intrinsicWidth.toFloat() / drawable.intrinsicHeight.toFloat() 170 | 171 | var left = 0f 172 | var top = 0f 173 | var right = imagePreviewWidth.toFloat() 174 | var bottom = imagePreviewHeight.toFloat() 175 | 176 | if (imageRatio > imageViewRatio) { 177 | // if the image is really wide, there's blank space at the top and bottom 178 | val offset = (imagePreviewHeight - (imagePreviewWidth / imageRatio)) / 2 179 | top += offset 180 | bottom -= offset 181 | } else { 182 | // if the image is really tall, there's blank space at the left and right 183 | // it's also possible that the image ratio matches the image container ratio 184 | // in which case there's no blank space 185 | val offset = (imagePreviewWidth - (imagePreviewHeight * imageRatio)) / 2 186 | left += offset 187 | right -= offset 188 | } 189 | 190 | return RectF(left, top, right, bottom) 191 | } 192 | 193 | /** 194 | * This ensures that the user doesn't drag a corner outside the image 195 | * 196 | * @param point a point 197 | * @return true if the point is inside the image preview container, false it's not 198 | */ 199 | private fun isPointInsideImage(point: PointF): Boolean { 200 | if (point.x >= imagePreviewBounds.left 201 | && point.y >= imagePreviewBounds.top 202 | && point.x <= imagePreviewBounds.right 203 | && point.y <= imagePreviewBounds.bottom) { 204 | return true 205 | } 206 | 207 | return false 208 | } 209 | 210 | /** 211 | * This gets called once we insert an image in this image view 212 | */ 213 | private fun onSetImage() { 214 | cropperSelectedCornerFillStyles.style = Paint.Style.FILL 215 | cropperSelectedCornerFillStyles.shader = BitmapShader( 216 | drawable.toBitmap(), 217 | Shader.TileMode.CLAMP, 218 | Shader.TileMode.CLAMP 219 | ) 220 | } 221 | 222 | /** 223 | * This gets called constantly, and we use it to update the cropper corners 224 | * 225 | * @param canvas the image preview canvas 226 | */ 227 | override fun onDraw(canvas: Canvas) { 228 | super.onDraw(canvas) 229 | 230 | if (quad !== null) { 231 | // draw 4 corners and connecting lines 232 | canvas.drawQuad( 233 | quad!!, 234 | resources.getDimension(R.dimen.cropper_corner_radius), 235 | cropperLinesAndCornersStyles, 236 | cropperSelectedCornerFillStyles, 237 | closestCornerToTouch, 238 | imagePreviewBounds, 239 | ratio, 240 | resources.getDimension(R.dimen.cropper_selected_corner_radius_magnification), 241 | resources.getDimension(R.dimen.cropper_selected_corner_background_magnification) 242 | ) 243 | } 244 | 245 | } 246 | 247 | /** 248 | * This gets called when the user touches, drags, and stops touching screen. We use this 249 | * to figure out which corner we need to move, and how far we need to move it. 250 | * 251 | * @param event the touch event 252 | */ 253 | @SuppressLint("ClickableViewAccessibility") 254 | override fun onTouchEvent(event: MotionEvent): Boolean { 255 | // keep track of the touched point 256 | val touchPoint = PointF(event.x, event.y) 257 | 258 | when (event.action) { 259 | MotionEvent.ACTION_DOWN -> { 260 | // when the user touches the screen record the point, and find the closest 261 | // corner to the touch point 262 | prevTouchPoint = touchPoint 263 | closestCornerToTouch = quad!!.getCornerClosestToPoint(touchPoint) 264 | } 265 | MotionEvent.ACTION_UP -> { 266 | // when the user stops touching the screen reset these values 267 | prevTouchPoint = null 268 | closestCornerToTouch = null 269 | } 270 | MotionEvent.ACTION_MOVE -> { 271 | // when the user drags their finger, update the closest corner position 272 | val touchMoveXDistance = touchPoint.x - prevTouchPoint!!.x 273 | val touchMoveYDistance = touchPoint.y - prevTouchPoint!!.y 274 | val cornerNewPosition = PointF( 275 | quad!!.corners[closestCornerToTouch]!!.x + touchMoveXDistance, 276 | quad!!.corners[closestCornerToTouch]!!.y + touchMoveYDistance 277 | ) 278 | 279 | // make sure the user doesn't drag the corner outside the image preview container 280 | if (isPointInsideImage(cornerNewPosition)) { 281 | quad!!.moveCorner(closestCornerToTouch!!, touchMoveXDistance, touchMoveYDistance) 282 | } 283 | 284 | // record the point touched, so we can use it to calculate how far to move corner 285 | // next time the user drags (assuming they don't stop touching the screen) 286 | prevTouchPoint = touchPoint 287 | } 288 | } 289 | 290 | // force refresh view 291 | invalidate() 292 | 293 | return true 294 | } 295 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/DocumentScannerActivity.kt: -------------------------------------------------------------------------------- 1 | package biz.cunning.cunning_document_scanner.fallback 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.view.View 9 | import android.widget.ImageButton 10 | import androidx.appcompat.app.AppCompatActivity 11 | import biz.cunning.cunning_document_scanner.R 12 | import biz.cunning.cunning_document_scanner.fallback.constants.DefaultSetting 13 | import biz.cunning.cunning_document_scanner.fallback.constants.DocumentScannerExtra 14 | import biz.cunning.cunning_document_scanner.fallback.extensions.move 15 | import biz.cunning.cunning_document_scanner.fallback.extensions.onClick 16 | import biz.cunning.cunning_document_scanner.fallback.extensions.saveToFile 17 | import biz.cunning.cunning_document_scanner.fallback.extensions.screenHeight 18 | import biz.cunning.cunning_document_scanner.fallback.extensions.screenWidth 19 | import biz.cunning.cunning_document_scanner.fallback.models.Document 20 | import biz.cunning.cunning_document_scanner.fallback.models.Point 21 | import biz.cunning.cunning_document_scanner.fallback.models.Quad 22 | import biz.cunning.cunning_document_scanner.fallback.ui.ImageCropView 23 | import biz.cunning.cunning_document_scanner.fallback.utils.CameraUtil 24 | import biz.cunning.cunning_document_scanner.fallback.utils.FileUtil 25 | import biz.cunning.cunning_document_scanner.fallback.utils.ImageUtil 26 | import java.io.File 27 | /** 28 | * This class contains the main document scanner code. It opens the camera, lets the user 29 | * take a photo of a document (homework paper, business card, etc.), detects document corners, 30 | * allows user to make adjustments to the detected corners, depending on options, and saves 31 | * the cropped document. It allows the user to do this for 1 or more documents. 32 | * 33 | * @constructor creates document scanner activity 34 | */ 35 | class DocumentScannerActivity : AppCompatActivity() { 36 | /** 37 | * @property maxNumDocuments maximum number of documents a user can scan at a time 38 | */ 39 | private var maxNumDocuments = DefaultSetting.MAX_NUM_DOCUMENTS 40 | 41 | /** 42 | * @property croppedImageQuality the 0 - 100 quality of the cropped image 43 | */ 44 | private var croppedImageQuality = DefaultSetting.CROPPED_IMAGE_QUALITY 45 | 46 | /** 47 | * @property cropperOffsetWhenCornersNotFound if we can't find document corners, we set 48 | * corners to image size with a slight margin 49 | */ 50 | private val cropperOffsetWhenCornersNotFound = 100.0 51 | 52 | /** 53 | * @property document This is the current document. Initially it's null. Once we capture 54 | * the photo, and find the corners we update document. 55 | */ 56 | private var document: Document? = null 57 | 58 | /** 59 | * @property documents a list of documents (original photo file path, original photo 60 | * dimensions and 4 corner points) 61 | */ 62 | private val documents = mutableListOf() 63 | 64 | /** 65 | * @property cameraUtil gets called with photo file path once user takes photo, or 66 | * exits camera 67 | */ 68 | private val cameraUtil = CameraUtil( 69 | this, 70 | onPhotoCaptureSuccess = { 71 | // user takes photo 72 | originalPhotoPath -> 73 | 74 | // if maxNumDocuments is 3 and this is the 3rd photo, hide the new photo button since 75 | // we reach the allowed limit 76 | if (documents.size == maxNumDocuments - 1) { 77 | val newPhotoButton: ImageButton = findViewById(R.id.new_photo_button) 78 | newPhotoButton.isClickable = false 79 | newPhotoButton.visibility = View.INVISIBLE 80 | } 81 | 82 | // get bitmap from photo file path 83 | val photo: Bitmap? = try { 84 | ImageUtil().getImageFromFilePath(originalPhotoPath) 85 | } catch (exception: Exception) { 86 | finishIntentWithError("Unable to get bitmap: ${exception.localizedMessage}") 87 | return@CameraUtil 88 | } 89 | 90 | if (photo == null) { 91 | finishIntentWithError("Document bitmap is null.") 92 | return@CameraUtil 93 | } 94 | 95 | // get document corners by detecting them, or falling back to photo corners with 96 | // slight margin if we can't find the corners 97 | val corners = try { 98 | val (topLeft, topRight, bottomLeft, bottomRight) = getDocumentCorners(photo) 99 | Quad(topLeft, topRight, bottomRight, bottomLeft) 100 | } catch (exception: Exception) { 101 | finishIntentWithError( 102 | "unable to get document corners: ${exception.message}" 103 | ) 104 | return@CameraUtil 105 | } 106 | 107 | document = Document(originalPhotoPath, photo.width, photo.height, corners) 108 | 109 | 110 | // user is allowed to move corners to make corrections 111 | try { 112 | // set preview image height based off of photo dimensions 113 | imageView.setImagePreviewBounds(photo, screenWidth, screenHeight) 114 | 115 | // display original photo, so user can adjust detected corners 116 | imageView.setImage(photo) 117 | 118 | // document corner points are in original image coordinates, so we need to 119 | // scale and move the points to account for blank space (caused by photo and 120 | // photo container having different aspect ratios) 121 | val cornersInImagePreviewCoordinates = corners 122 | .mapOriginalToPreviewImageCoordinates( 123 | imageView.imagePreviewBounds, 124 | imageView.imagePreviewBounds.height() / photo.height 125 | ) 126 | 127 | // display cropper, and allow user to move corners 128 | imageView.setCropper(cornersInImagePreviewCoordinates) 129 | } catch (exception: Exception) { 130 | finishIntentWithError( 131 | "unable get image preview ready: ${exception.message}" 132 | ) 133 | return@CameraUtil 134 | } 135 | }, 136 | onCancelPhoto = { 137 | // user exits camera 138 | // complete document scan if this is the first document since we can't go to crop view 139 | // until user takes at least 1 photo 140 | if (documents.isEmpty()) { 141 | onClickCancel() 142 | } 143 | } 144 | ) 145 | 146 | /** 147 | * @property imageView container with original photo and cropper 148 | */ 149 | private lateinit var imageView: ImageCropView 150 | 151 | /** 152 | * called when activity is created 153 | * 154 | * @param savedInstanceState persisted data that maintains state 155 | */ 156 | override fun onCreate(savedInstanceState: Bundle?) { 157 | super.onCreate(savedInstanceState) 158 | 159 | // Show cropper, accept crop button, add new document button, and 160 | // retake photo button. Since we open the camera in a few lines, the user 161 | // doesn't see this until they finish taking a photo 162 | setContentView(R.layout.activity_image_crop) 163 | imageView = findViewById(R.id.image_view) 164 | 165 | try { 166 | // validate maxNumDocuments option, and update default if user sets it 167 | var userSpecifiedMaxImages: Int? = null 168 | intent.extras?.get(DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS)?.let { 169 | if (it.toString().toIntOrNull() == null) { 170 | throw Exception( 171 | "${DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS} must be a positive number" 172 | ) 173 | } 174 | userSpecifiedMaxImages = it as Int 175 | maxNumDocuments = userSpecifiedMaxImages as Int 176 | } 177 | 178 | // validate croppedImageQuality option, and update value if user sets it 179 | intent.extras?.get(DocumentScannerExtra.EXTRA_CROPPED_IMAGE_QUALITY)?.let { 180 | if (it !is Int || it < 0 || it > 100) { 181 | throw Exception( 182 | "${DocumentScannerExtra.EXTRA_CROPPED_IMAGE_QUALITY} must be a number " + 183 | "between 0 and 100" 184 | ) 185 | } 186 | croppedImageQuality = it 187 | } 188 | } catch (exception: Exception) { 189 | finishIntentWithError( 190 | "invalid extra: ${exception.message}" 191 | ) 192 | return 193 | } 194 | 195 | // set click event handlers for new document button, accept and crop document button, 196 | // and retake document photo button 197 | val newPhotoButton: ImageButton = findViewById(R.id.new_photo_button) 198 | val completeDocumentScanButton: ImageButton = findViewById( 199 | R.id.complete_document_scan_button 200 | ) 201 | val retakePhotoButton: ImageButton = findViewById(R.id.retake_photo_button) 202 | 203 | newPhotoButton.onClick { onClickNew() } 204 | completeDocumentScanButton.onClick { onClickDone() } 205 | retakePhotoButton.onClick { onClickRetake() } 206 | 207 | // open camera, so user can snap document photo 208 | try { 209 | openCamera() 210 | } catch (exception: Exception) { 211 | finishIntentWithError( 212 | "error opening camera: ${exception.message}" 213 | ) 214 | } 215 | } 216 | 217 | /** 218 | * Pass in a photo of a document, and get back 4 corner points (top left, top right, bottom 219 | * right, bottom left). This tries to detect document corners, but falls back to photo corners 220 | * with slight margin in case we can't detect document corners. 221 | * 222 | * @param photo the original photo with a rectangular document 223 | * @return a List of 4 OpenCV points (document corners) 224 | */ 225 | private fun getDocumentCorners(photo: Bitmap): List { 226 | val cornerPoints: List? = null 227 | 228 | // if cornerPoints is null then default the corners to the photo bounds with a margin 229 | return cornerPoints ?: listOf( 230 | Point(0.0, 0.0).move( 231 | cropperOffsetWhenCornersNotFound, 232 | cropperOffsetWhenCornersNotFound 233 | ), 234 | Point(photo.width.toDouble(), 0.0).move( 235 | -cropperOffsetWhenCornersNotFound, 236 | cropperOffsetWhenCornersNotFound 237 | ), 238 | Point(0.0, photo.height.toDouble()).move( 239 | cropperOffsetWhenCornersNotFound, 240 | -cropperOffsetWhenCornersNotFound 241 | ), 242 | Point(photo.width.toDouble(), photo.height.toDouble()).move( 243 | -cropperOffsetWhenCornersNotFound, 244 | -cropperOffsetWhenCornersNotFound 245 | ) 246 | ) 247 | } 248 | 249 | /** 250 | * Set document to null since we're capturing a new document, and open the camera. If the 251 | * user captures a photo successfully document gets updated. 252 | */ 253 | private fun openCamera() { 254 | document = null 255 | cameraUtil.openCamera(documents.size) 256 | } 257 | 258 | /** 259 | * Once user accepts by pressing check button, or by pressing add new document button, add 260 | * original photo path and 4 document corners to documents list. If user isn't allowed to 261 | * adjust corners, call this automatically. 262 | */ 263 | private fun addSelectedCornersAndOriginalPhotoPathToDocuments() { 264 | // only add document it's not null (the current document photo capture, and corner 265 | // detection are successful) 266 | document?.let { document -> 267 | // convert corners from image preview coordinates to original photo coordinates 268 | // (original image is probably bigger than the preview image) 269 | val cornersInOriginalImageCoordinates = imageView.corners 270 | .mapPreviewToOriginalImageCoordinates( 271 | imageView.imagePreviewBounds, 272 | imageView.imagePreviewBounds.height() / document.originalPhotoHeight 273 | ) 274 | document.corners = cornersInOriginalImageCoordinates 275 | documents.add(document) 276 | } 277 | } 278 | 279 | /** 280 | * This gets called when a user presses the new document button. Store current photo path 281 | * with document corners. Then open the camera, so user can take a photo of the next 282 | * page or document 283 | */ 284 | private fun onClickNew() { 285 | addSelectedCornersAndOriginalPhotoPathToDocuments() 286 | openCamera() 287 | } 288 | 289 | /** 290 | * This gets called when a user presses the done button. Store current photo path with 291 | * document corners. Then crop document using corners, and return cropped image paths 292 | */ 293 | private fun onClickDone() { 294 | addSelectedCornersAndOriginalPhotoPathToDocuments() 295 | cropDocumentAndFinishIntent() 296 | } 297 | 298 | /** 299 | * This gets called when a user presses the retake photo button. The user presses this in 300 | * case the original document photo isn't good, and they need to take it again. 301 | */ 302 | private fun onClickRetake() { 303 | // we're going to retake the photo, so delete the one we just took 304 | document?.let { document -> File(document.originalPhotoFilePath).delete() } 305 | openCamera() 306 | } 307 | 308 | /** 309 | * This gets called when a user doesn't want to complete the document scan after starting. 310 | * For example a user can quit out of the camera before snapping a photo of the document. 311 | */ 312 | private fun onClickCancel() { 313 | setResult(Activity.RESULT_CANCELED) 314 | finish() 315 | } 316 | 317 | /** 318 | * This crops original document photo, saves cropped document photo, deletes original 319 | * document photo, and returns cropped document photo file path. It repeats that for 320 | * all document photos. 321 | */ 322 | private fun cropDocumentAndFinishIntent() { 323 | val croppedImageResults = arrayListOf() 324 | for ((pageNumber, document) in documents.withIndex()) { 325 | // crop document photo by using corners 326 | val croppedImage: Bitmap? = try { 327 | ImageUtil().crop( 328 | document.originalPhotoFilePath, 329 | document.corners 330 | ) 331 | } catch (exception: Exception) { 332 | finishIntentWithError("unable to crop image: ${exception.message}") 333 | return 334 | } 335 | 336 | if (croppedImage == null) { 337 | finishIntentWithError("Result of cropping is null") 338 | return 339 | } 340 | 341 | // delete original document photo 342 | File(document.originalPhotoFilePath).delete() 343 | 344 | // save cropped document photo 345 | try { 346 | val croppedImageFile = FileUtil().createImageFile(this, pageNumber) 347 | croppedImage.saveToFile(croppedImageFile, croppedImageQuality) 348 | croppedImageResults.add(Uri.fromFile(croppedImageFile).toString()) 349 | } catch (exception: Exception) { 350 | finishIntentWithError( 351 | "unable to save cropped image: ${exception.message}" 352 | ) 353 | } 354 | } 355 | 356 | // return array of cropped document photo file paths 357 | setResult( 358 | Activity.RESULT_OK, 359 | Intent().putExtra("croppedImageResults", croppedImageResults) 360 | ) 361 | finish() 362 | } 363 | 364 | /** 365 | * This ends the document scanner activity, and returns an error message that can be 366 | * used to debug error 367 | * 368 | * @param errorMessage an error message 369 | */ 370 | private fun finishIntentWithError(errorMessage: String) { 371 | setResult( 372 | Activity.RESULT_OK, 373 | Intent().putExtra("error", errorMessage) 374 | ) 375 | finish() 376 | } 377 | } -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 6999D16A5FD7B255987132A6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A4E45770847E700724689E1 /* Pods_Runner.framework */; }; 13 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 14 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 15 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 16 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 34 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 35 | 1A75B21102973CE41A6E8E7B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 36 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 37 | 4D5213E4E9A902D4BFDA66A6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 38 | 554ACEF25A741CCBB5AC6F5D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 39 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 40 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 41 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 42 | 8A4E45770847E700724689E1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 44 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 45 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 47 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | 6999D16A5FD7B255987132A6 /* Pods_Runner.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 87C701EE362EC03CC51D3AD0 /* Frameworks */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 8A4E45770847E700724689E1 /* Pods_Runner.framework */, 68 | ); 69 | name = Frameworks; 70 | sourceTree = ""; 71 | }; 72 | 9740EEB11CF90186004384FC /* Flutter */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 76 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 77 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 78 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 79 | ); 80 | name = Flutter; 81 | sourceTree = ""; 82 | }; 83 | 97C146E51CF9000F007C117D = { 84 | isa = PBXGroup; 85 | children = ( 86 | 9740EEB11CF90186004384FC /* Flutter */, 87 | 97C146F01CF9000F007C117D /* Runner */, 88 | 97C146EF1CF9000F007C117D /* Products */, 89 | C304058BD58D8DFA9B2DF840 /* Pods */, 90 | 87C701EE362EC03CC51D3AD0 /* Frameworks */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | 97C146EF1CF9000F007C117D /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 97C146EE1CF9000F007C117D /* Runner.app */, 98 | ); 99 | name = Products; 100 | sourceTree = ""; 101 | }; 102 | 97C146F01CF9000F007C117D /* Runner */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 106 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 107 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 108 | 97C147021CF9000F007C117D /* Info.plist */, 109 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 110 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 111 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 112 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 113 | ); 114 | path = Runner; 115 | sourceTree = ""; 116 | }; 117 | C304058BD58D8DFA9B2DF840 /* Pods */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 4D5213E4E9A902D4BFDA66A6 /* Pods-Runner.debug.xcconfig */, 121 | 1A75B21102973CE41A6E8E7B /* Pods-Runner.release.xcconfig */, 122 | 554ACEF25A741CCBB5AC6F5D /* Pods-Runner.profile.xcconfig */, 123 | ); 124 | path = Pods; 125 | sourceTree = ""; 126 | }; 127 | /* End PBXGroup section */ 128 | 129 | /* Begin PBXNativeTarget section */ 130 | 97C146ED1CF9000F007C117D /* Runner */ = { 131 | isa = PBXNativeTarget; 132 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 133 | buildPhases = ( 134 | 86D6D3EC8B514B0DEC99F3BC /* [CP] Check Pods Manifest.lock */, 135 | 9740EEB61CF901F6004384FC /* Run Script */, 136 | 97C146EA1CF9000F007C117D /* Sources */, 137 | 97C146EB1CF9000F007C117D /* Frameworks */, 138 | 97C146EC1CF9000F007C117D /* Resources */, 139 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 140 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 141 | 7C0BF88075F6ADB364285AE1 /* [CP] Embed Pods Frameworks */, 142 | DE539EF0689856B854BD2C91 /* [CP] Copy Pods Resources */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | ); 148 | name = Runner; 149 | productName = Runner; 150 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 151 | productType = "com.apple.product-type.application"; 152 | }; 153 | /* End PBXNativeTarget section */ 154 | 155 | /* Begin PBXProject section */ 156 | 97C146E61CF9000F007C117D /* Project object */ = { 157 | isa = PBXProject; 158 | attributes = { 159 | LastUpgradeCheck = 1510; 160 | ORGANIZATIONNAME = ""; 161 | TargetAttributes = { 162 | 97C146ED1CF9000F007C117D = { 163 | CreatedOnToolsVersion = 7.3.1; 164 | LastSwiftMigration = 1100; 165 | }; 166 | }; 167 | }; 168 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 169 | compatibilityVersion = "Xcode 9.3"; 170 | developmentRegion = en; 171 | hasScannedForEncodings = 0; 172 | knownRegions = ( 173 | en, 174 | Base, 175 | ); 176 | mainGroup = 97C146E51CF9000F007C117D; 177 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 97C146ED1CF9000F007C117D /* Runner */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 97C146EC1CF9000F007C117D /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 192 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 193 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 194 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXResourcesBuildPhase section */ 199 | 200 | /* Begin PBXShellScriptBuildPhase section */ 201 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 202 | isa = PBXShellScriptBuildPhase; 203 | alwaysOutOfDate = 1; 204 | buildActionMask = 2147483647; 205 | files = ( 206 | ); 207 | inputPaths = ( 208 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 209 | ); 210 | name = "Thin Binary"; 211 | outputPaths = ( 212 | ); 213 | runOnlyForDeploymentPostprocessing = 0; 214 | shellPath = /bin/sh; 215 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 216 | }; 217 | 7C0BF88075F6ADB364285AE1 /* [CP] Embed Pods Frameworks */ = { 218 | isa = PBXShellScriptBuildPhase; 219 | buildActionMask = 2147483647; 220 | files = ( 221 | ); 222 | inputFileListPaths = ( 223 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 224 | ); 225 | name = "[CP] Embed Pods Frameworks"; 226 | outputFileListPaths = ( 227 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | shellPath = /bin/sh; 231 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 232 | showEnvVarsInLog = 0; 233 | }; 234 | 86D6D3EC8B514B0DEC99F3BC /* [CP] Check Pods Manifest.lock */ = { 235 | isa = PBXShellScriptBuildPhase; 236 | buildActionMask = 2147483647; 237 | files = ( 238 | ); 239 | inputFileListPaths = ( 240 | ); 241 | inputPaths = ( 242 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 243 | "${PODS_ROOT}/Manifest.lock", 244 | ); 245 | name = "[CP] Check Pods Manifest.lock"; 246 | outputFileListPaths = ( 247 | ); 248 | outputPaths = ( 249 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | shellPath = /bin/sh; 253 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 254 | showEnvVarsInLog = 0; 255 | }; 256 | 9740EEB61CF901F6004384FC /* Run Script */ = { 257 | isa = PBXShellScriptBuildPhase; 258 | alwaysOutOfDate = 1; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | ); 262 | inputPaths = ( 263 | ); 264 | name = "Run Script"; 265 | outputPaths = ( 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | shellPath = /bin/sh; 269 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 270 | }; 271 | DE539EF0689856B854BD2C91 /* [CP] Copy Pods Resources */ = { 272 | isa = PBXShellScriptBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | ); 276 | inputFileListPaths = ( 277 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", 278 | ); 279 | name = "[CP] Copy Pods Resources"; 280 | outputFileListPaths = ( 281 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", 282 | ); 283 | runOnlyForDeploymentPostprocessing = 0; 284 | shellPath = /bin/sh; 285 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; 286 | showEnvVarsInLog = 0; 287 | }; 288 | /* End PBXShellScriptBuildPhase section */ 289 | 290 | /* Begin PBXSourcesBuildPhase section */ 291 | 97C146EA1CF9000F007C117D /* Sources */ = { 292 | isa = PBXSourcesBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 296 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 297 | ); 298 | runOnlyForDeploymentPostprocessing = 0; 299 | }; 300 | /* End PBXSourcesBuildPhase section */ 301 | 302 | /* Begin PBXVariantGroup section */ 303 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 304 | isa = PBXVariantGroup; 305 | children = ( 306 | 97C146FB1CF9000F007C117D /* Base */, 307 | ); 308 | name = Main.storyboard; 309 | sourceTree = ""; 310 | }; 311 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 312 | isa = PBXVariantGroup; 313 | children = ( 314 | 97C147001CF9000F007C117D /* Base */, 315 | ); 316 | name = LaunchScreen.storyboard; 317 | sourceTree = ""; 318 | }; 319 | /* End PBXVariantGroup section */ 320 | 321 | /* Begin XCBuildConfiguration section */ 322 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 323 | isa = XCBuildConfiguration; 324 | buildSettings = { 325 | ALWAYS_SEARCH_USER_PATHS = NO; 326 | CLANG_ANALYZER_NONNULL = YES; 327 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 328 | CLANG_CXX_LIBRARY = "libc++"; 329 | CLANG_ENABLE_MODULES = YES; 330 | CLANG_ENABLE_OBJC_ARC = YES; 331 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 332 | CLANG_WARN_BOOL_CONVERSION = YES; 333 | CLANG_WARN_COMMA = YES; 334 | CLANG_WARN_CONSTANT_CONVERSION = YES; 335 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 336 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 337 | CLANG_WARN_EMPTY_BODY = YES; 338 | CLANG_WARN_ENUM_CONVERSION = YES; 339 | CLANG_WARN_INFINITE_RECURSION = YES; 340 | CLANG_WARN_INT_CONVERSION = YES; 341 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 342 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 343 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 344 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 345 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 346 | CLANG_WARN_STRICT_PROTOTYPES = YES; 347 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 348 | CLANG_WARN_UNREACHABLE_CODE = YES; 349 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 350 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 351 | COPY_PHASE_STRIP = NO; 352 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 353 | ENABLE_NS_ASSERTIONS = NO; 354 | ENABLE_STRICT_OBJC_MSGSEND = YES; 355 | GCC_C_LANGUAGE_STANDARD = gnu99; 356 | GCC_NO_COMMON_BLOCKS = YES; 357 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 358 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 359 | GCC_WARN_UNDECLARED_SELECTOR = YES; 360 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 361 | GCC_WARN_UNUSED_FUNCTION = YES; 362 | GCC_WARN_UNUSED_VARIABLE = YES; 363 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 364 | MTL_ENABLE_DEBUG_INFO = NO; 365 | SDKROOT = iphoneos; 366 | SUPPORTED_PLATFORMS = iphoneos; 367 | TARGETED_DEVICE_FAMILY = "1,2"; 368 | VALIDATE_PRODUCT = YES; 369 | }; 370 | name = Profile; 371 | }; 372 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 373 | isa = XCBuildConfiguration; 374 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 375 | buildSettings = { 376 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 377 | CLANG_ENABLE_MODULES = YES; 378 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 379 | DEVELOPMENT_TEAM = F8R24LCAZ8; 380 | ENABLE_BITCODE = NO; 381 | INFOPLIST_FILE = Runner/Info.plist; 382 | LD_RUNPATH_SEARCH_PATHS = ( 383 | "$(inherited)", 384 | "@executable_path/Frameworks", 385 | ); 386 | PRODUCT_BUNDLE_IDENTIFIER = biz.cunning.cunningDocumentScannerExample; 387 | PRODUCT_NAME = "$(TARGET_NAME)"; 388 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 389 | SWIFT_VERSION = 5.0; 390 | VERSIONING_SYSTEM = "apple-generic"; 391 | }; 392 | name = Profile; 393 | }; 394 | 97C147031CF9000F007C117D /* Debug */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | ALWAYS_SEARCH_USER_PATHS = NO; 398 | CLANG_ANALYZER_NONNULL = YES; 399 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 400 | CLANG_CXX_LIBRARY = "libc++"; 401 | CLANG_ENABLE_MODULES = YES; 402 | CLANG_ENABLE_OBJC_ARC = YES; 403 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 404 | CLANG_WARN_BOOL_CONVERSION = YES; 405 | CLANG_WARN_COMMA = YES; 406 | CLANG_WARN_CONSTANT_CONVERSION = YES; 407 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 408 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 409 | CLANG_WARN_EMPTY_BODY = YES; 410 | CLANG_WARN_ENUM_CONVERSION = YES; 411 | CLANG_WARN_INFINITE_RECURSION = YES; 412 | CLANG_WARN_INT_CONVERSION = YES; 413 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 415 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 417 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 418 | CLANG_WARN_STRICT_PROTOTYPES = YES; 419 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 420 | CLANG_WARN_UNREACHABLE_CODE = YES; 421 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 422 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 423 | COPY_PHASE_STRIP = NO; 424 | DEBUG_INFORMATION_FORMAT = dwarf; 425 | ENABLE_STRICT_OBJC_MSGSEND = YES; 426 | ENABLE_TESTABILITY = YES; 427 | GCC_C_LANGUAGE_STANDARD = gnu99; 428 | GCC_DYNAMIC_NO_PIC = NO; 429 | GCC_NO_COMMON_BLOCKS = YES; 430 | GCC_OPTIMIZATION_LEVEL = 0; 431 | GCC_PREPROCESSOR_DEFINITIONS = ( 432 | "DEBUG=1", 433 | "$(inherited)", 434 | ); 435 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 436 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 437 | GCC_WARN_UNDECLARED_SELECTOR = YES; 438 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 439 | GCC_WARN_UNUSED_FUNCTION = YES; 440 | GCC_WARN_UNUSED_VARIABLE = YES; 441 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 442 | MTL_ENABLE_DEBUG_INFO = YES; 443 | ONLY_ACTIVE_ARCH = YES; 444 | SDKROOT = iphoneos; 445 | TARGETED_DEVICE_FAMILY = "1,2"; 446 | }; 447 | name = Debug; 448 | }; 449 | 97C147041CF9000F007C117D /* Release */ = { 450 | isa = XCBuildConfiguration; 451 | buildSettings = { 452 | ALWAYS_SEARCH_USER_PATHS = NO; 453 | CLANG_ANALYZER_NONNULL = YES; 454 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 455 | CLANG_CXX_LIBRARY = "libc++"; 456 | CLANG_ENABLE_MODULES = YES; 457 | CLANG_ENABLE_OBJC_ARC = YES; 458 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 459 | CLANG_WARN_BOOL_CONVERSION = YES; 460 | CLANG_WARN_COMMA = YES; 461 | CLANG_WARN_CONSTANT_CONVERSION = YES; 462 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 463 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 464 | CLANG_WARN_EMPTY_BODY = YES; 465 | CLANG_WARN_ENUM_CONVERSION = YES; 466 | CLANG_WARN_INFINITE_RECURSION = YES; 467 | CLANG_WARN_INT_CONVERSION = YES; 468 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 469 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 470 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 471 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 472 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 473 | CLANG_WARN_STRICT_PROTOTYPES = YES; 474 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 475 | CLANG_WARN_UNREACHABLE_CODE = YES; 476 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 477 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 478 | COPY_PHASE_STRIP = NO; 479 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 480 | ENABLE_NS_ASSERTIONS = NO; 481 | ENABLE_STRICT_OBJC_MSGSEND = YES; 482 | GCC_C_LANGUAGE_STANDARD = gnu99; 483 | GCC_NO_COMMON_BLOCKS = YES; 484 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 485 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 486 | GCC_WARN_UNDECLARED_SELECTOR = YES; 487 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 488 | GCC_WARN_UNUSED_FUNCTION = YES; 489 | GCC_WARN_UNUSED_VARIABLE = YES; 490 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 491 | MTL_ENABLE_DEBUG_INFO = NO; 492 | SDKROOT = iphoneos; 493 | SUPPORTED_PLATFORMS = iphoneos; 494 | SWIFT_COMPILATION_MODE = wholemodule; 495 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 496 | TARGETED_DEVICE_FAMILY = "1,2"; 497 | VALIDATE_PRODUCT = YES; 498 | }; 499 | name = Release; 500 | }; 501 | 97C147061CF9000F007C117D /* Debug */ = { 502 | isa = XCBuildConfiguration; 503 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 504 | buildSettings = { 505 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 506 | CLANG_ENABLE_MODULES = YES; 507 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 508 | DEVELOPMENT_TEAM = 7624MWN53C; 509 | ENABLE_BITCODE = NO; 510 | INFOPLIST_FILE = Runner/Info.plist; 511 | LD_RUNPATH_SEARCH_PATHS = ( 512 | "$(inherited)", 513 | "@executable_path/Frameworks", 514 | ); 515 | PRODUCT_BUNDLE_IDENTIFIER = biz.cunning.cunningDocumentScannerExample; 516 | PRODUCT_NAME = "$(TARGET_NAME)"; 517 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 518 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 519 | SWIFT_VERSION = 5.0; 520 | VERSIONING_SYSTEM = "apple-generic"; 521 | }; 522 | name = Debug; 523 | }; 524 | 97C147071CF9000F007C117D /* Release */ = { 525 | isa = XCBuildConfiguration; 526 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 527 | buildSettings = { 528 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 529 | CLANG_ENABLE_MODULES = YES; 530 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 531 | DEVELOPMENT_TEAM = F8R24LCAZ8; 532 | ENABLE_BITCODE = NO; 533 | INFOPLIST_FILE = Runner/Info.plist; 534 | LD_RUNPATH_SEARCH_PATHS = ( 535 | "$(inherited)", 536 | "@executable_path/Frameworks", 537 | ); 538 | PRODUCT_BUNDLE_IDENTIFIER = biz.cunning.cunningDocumentScannerExample; 539 | PRODUCT_NAME = "$(TARGET_NAME)"; 540 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 541 | SWIFT_VERSION = 5.0; 542 | VERSIONING_SYSTEM = "apple-generic"; 543 | }; 544 | name = Release; 545 | }; 546 | /* End XCBuildConfiguration section */ 547 | 548 | /* Begin XCConfigurationList section */ 549 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 550 | isa = XCConfigurationList; 551 | buildConfigurations = ( 552 | 97C147031CF9000F007C117D /* Debug */, 553 | 97C147041CF9000F007C117D /* Release */, 554 | 249021D3217E4FDB00AE95B9 /* Profile */, 555 | ); 556 | defaultConfigurationIsVisible = 0; 557 | defaultConfigurationName = Release; 558 | }; 559 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 560 | isa = XCConfigurationList; 561 | buildConfigurations = ( 562 | 97C147061CF9000F007C117D /* Debug */, 563 | 97C147071CF9000F007C117D /* Release */, 564 | 249021D4217E4FDB00AE95B9 /* Profile */, 565 | ); 566 | defaultConfigurationIsVisible = 0; 567 | defaultConfigurationName = Release; 568 | }; 569 | /* End XCConfigurationList section */ 570 | }; 571 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 572 | } 573 | --------------------------------------------------------------------------------