diff --git a/documentscanner/src/main/java/com/websitebeaver/documentscanner/DocumentScanner.kt b/documentscanner/src/main/java/com/websitebeaver/documentscanner/DocumentScanner.kt index 4fe5ff9..3896e36 100644 --- a/documentscanner/src/main/java/com/websitebeaver/documentscanner/DocumentScanner.kt +++ b/documentscanner/src/main/java/com/websitebeaver/documentscanner/DocumentScanner.kt @@ -24,6 +24,7 @@ import java.io.File * @param letUserAdjustCrop whether or not the user can change the auto detected document corners * @param maxNumDocuments the maximum number of documents a user can scan at once * @param croppedImageQuality the 0 - 100 quality of the cropped image + * @param imageProvider whether to use the camera or gallery to choose documents * @constructor creates document scanner */ class DocumentScanner( @@ -34,7 +35,8 @@ class DocumentScanner( private var responseType: String? = null, private var letUserAdjustCrop: Boolean? = null, private var maxNumDocuments: Int? = null, - private var croppedImageQuality: Int? = null + private var croppedImageQuality: Int? = null, + private var imageProvider: String? = null ) { init { responseType = responseType ?: DefaultSetting.RESPONSE_TYPE @@ -58,6 +60,10 @@ class DocumentScanner( DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS, maxNumDocuments ) + documentScanIntent.putExtra( + DocumentScannerExtra.EXTRA_IMAGE_PROVIDER, + imageProvider + ) return documentScanIntent } diff --git a/documentscanner/src/main/java/com/websitebeaver/documentscanner/DocumentScannerActivity.kt b/documentscanner/src/main/java/com/websitebeaver/documentscanner/DocumentScannerActivity.kt index 48d9a95..0e39041 100644 --- a/documentscanner/src/main/java/com/websitebeaver/documentscanner/DocumentScannerActivity.kt +++ b/documentscanner/src/main/java/com/websitebeaver/documentscanner/DocumentScannerActivity.kt @@ -10,6 +10,7 @@ import android.widget.ImageButton import androidx.appcompat.app.AppCompatActivity import com.websitebeaver.documentscanner.constants.DefaultSetting import com.websitebeaver.documentscanner.constants.DocumentScannerExtra +import com.websitebeaver.documentscanner.constants.ImageProvider import com.websitebeaver.documentscanner.extensions.move import com.websitebeaver.documentscanner.extensions.onClick import com.websitebeaver.documentscanner.extensions.saveToFile @@ -20,6 +21,7 @@ import com.websitebeaver.documentscanner.models.Quad import com.websitebeaver.documentscanner.ui.ImageCropView import com.websitebeaver.documentscanner.utils.CameraUtil import com.websitebeaver.documentscanner.utils.FileUtil +import com.websitebeaver.documentscanner.utils.GalleryUtil import com.websitebeaver.documentscanner.utils.ImageUtil import java.io.File import org.opencv.core.Point @@ -48,6 +50,11 @@ class DocumentScannerActivity : AppCompatActivity() { */ private var croppedImageQuality = DefaultSetting.CROPPED_IMAGE_QUALITY + /** + * @property imageProvider whether to use the camera or gallery to choose documents + */ + private var imageProvider = DefaultSetting.IMAGE_PROVIDER + /** * @property cropperOffsetWhenCornersNotFound if we can't find document corners, we set * corners to image size with a slight margin @@ -147,6 +154,87 @@ class DocumentScannerActivity : AppCompatActivity() { } ) + /** + * @property galleryUtil gets called with photo file path once user chooses image, or + * exits gallery + */ + private val galleryUtil = GalleryUtil( + this, + onGallerySuccess = { + // user chooses photo + originalPhotoPath -> + + // if maxNumDocuments is 3 and this is the 3rd photo, hide the new photo button since + // we reach the allowed limit + if (documents.size == maxNumDocuments - 1) { + val newPhotoButton: ImageButton = findViewById(R.id.new_photo_button) + newPhotoButton.isClickable = false + newPhotoButton.visibility = View.INVISIBLE + } + + // get bitmap from photo file path + val photo: Bitmap = ImageUtil().getImageFromFilePath(originalPhotoPath) + + // get document corners by detecting them, or falling back to photo corners with + // slight margin if we can't find the corners + val corners = try { + val (topLeft, topRight, bottomLeft, bottomRight) = getDocumentCorners(photo) + Quad(topLeft, topRight, bottomRight, bottomLeft) + } catch (exception: Exception) { + finishIntentWithError( + "unable to get document corners: ${exception.message}" + ) + return@GalleryUtil + } + + document = Document(originalPhotoPath, photo.width, photo.height, corners) + + if (letUserAdjustCrop) { + // user is allowed to move corners to make corrections + try { + // set preview image height based off of photo dimensions + imageView.setImagePreviewBounds(photo, screenWidth, screenHeight) + + // display original photo, so user can adjust detected corners + imageView.setImage(photo) + + // document corner points are in original image coordinates, so we need to + // scale and move the points to account for blank space (caused by photo and + // photo container having different aspect ratios) + val cornersInImagePreviewCoordinates = corners + .mapOriginalToPreviewImageCoordinates( + imageView.imagePreviewBounds, + imageView.imagePreviewBounds.height() / photo.height + ) + + // display cropper, and allow user to move corners + imageView.setCropper(cornersInImagePreviewCoordinates) + } catch (exception: Exception) { + finishIntentWithError( + "unable get image preview ready: ${exception.message}" + ) + return@GalleryUtil + } + } else { + // user isn't allowed to move corners, so accept automatically detected corners + document?.let { document -> + documents.add(document) + } + + // create cropped document image, and return file path to complete document scan + cropDocumentAndFinishIntent() + } + }, + onCancelGallery = { + // user exits gallery + // complete document scan if this is the first document since we can't go to crop view + // until user takes at least 1 photo + if (documents.isEmpty()) { + onClickCancel() + } + } + ) + /** * @property imageView container with original photo and cropper */ @@ -220,6 +308,14 @@ class DocumentScannerActivity : AppCompatActivity() { } croppedImageQuality = it } + + // validate imageProvider option, and update default if user sets it + intent.extras?.get(DocumentScannerExtra.EXTRA_IMAGE_PROVIDER)?.let { + if (!arrayOf(ImageProvider.CAMERA, ImageProvider.GALLERY).contains(it.toString())) { + throw Exception("${DocumentScannerExtra.EXTRA_LET_USER_ADJUST_CROP} must be either camera or gallery") + } + imageProvider = it as String + } } catch (exception: Exception) { finishIntentWithError( "invalid extra: ${exception.message}" @@ -239,13 +335,19 @@ class DocumentScannerActivity : AppCompatActivity() { completeDocumentScanButton.onClick { onClickDone() } retakePhotoButton.onClick { onClickRetake() } - // open camera, so user can snap document photo - try { - openCamera() - } catch (exception: Exception) { - finishIntentWithError( - "error opening camera: ${exception.message}" - ) + // open camera or gallery, so user can snap or choose document photo + if (imageProvider == ImageProvider.CAMERA) { + try { + openCamera() + } catch (exception: Exception) { + finishIntentWithError("error opening camera: ${exception.message}") + } + } else if (imageProvider == ImageProvider.GALLERY) { + try { + openGallery() + } catch (exception: Exception) { + finishIntentWithError("error opening gallery: ${exception.message}") + } } } @@ -290,6 +392,15 @@ class DocumentScannerActivity : AppCompatActivity() { cameraUtil.openCamera(documents.size) } + /** + * Set document to null since we're choosing a new document, and open the camera. If the + * user chooses a photo successfully document gets updated. + */ + private fun openGallery() { + document = null + galleryUtil.openGallery(documents.size) + } + /** * Once user accepts by pressing check button, or by pressing add new document button, add * original photo path and 4 document corners to documents list. If user isn't allowed to @@ -318,7 +429,11 @@ class DocumentScannerActivity : AppCompatActivity() { */ private fun onClickNew() { addSelectedCornersAndOriginalPhotoPathToDocuments() - openCamera() + if (imageProvider == ImageProvider.CAMERA) { + openCamera() + } else if (imageProvider == ImageProvider.GALLERY) { + openGallery() + } } /** @@ -337,7 +452,11 @@ class DocumentScannerActivity : AppCompatActivity() { private fun onClickRetake() { // we're going to retake the photo, so delete the one we just took document?.let { document -> File(document.originalPhotoFilePath).delete() } - openCamera() + if (imageProvider == ImageProvider.CAMERA) { + openCamera() + } else if (imageProvider == ImageProvider.GALLERY) { + openGallery() + } } /** diff --git a/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/DefaultSetting.kt b/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/DefaultSetting.kt index e22938f..b1680ec 100644 --- a/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/DefaultSetting.kt +++ b/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/DefaultSetting.kt @@ -9,5 +9,6 @@ class DefaultSetting { const val LET_USER_ADJUST_CROP = true const val MAX_NUM_DOCUMENTS = 24 const val RESPONSE_TYPE = ResponseType.IMAGE_FILE_PATH + const val IMAGE_PROVIDER = ImageProvider.CAMERA } } \ No newline at end of file diff --git a/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/DocumentScannerExtra.kt b/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/DocumentScannerExtra.kt index 442d455..152a6fb 100644 --- a/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/DocumentScannerExtra.kt +++ b/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/DocumentScannerExtra.kt @@ -8,5 +8,6 @@ class DocumentScannerExtra { const val EXTRA_CROPPED_IMAGE_QUALITY = "croppedImageQuality" const val EXTRA_LET_USER_ADJUST_CROP = "letUserAdjustCrop" const val EXTRA_MAX_NUM_DOCUMENTS = "maxNumDocuments" + const val EXTRA_IMAGE_PROVIDER = "imageProvider" } } \ No newline at end of file diff --git a/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/ImageProvider.kt b/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/ImageProvider.kt new file mode 100644 index 0000000..cb2cd5c --- /dev/null +++ b/documentscanner/src/main/java/com/websitebeaver/documentscanner/constants/ImageProvider.kt @@ -0,0 +1,9 @@ +package com.websitebeaver.documentscanner.constants + +/** constants that represent all image provider */ +class ImageProvider { + companion object { + const val CAMERA = "camera" + const val GALLERY = "gallery" + } +} \ No newline at end of file diff --git a/documentscanner/src/main/java/com/websitebeaver/documentscanner/utils/GalleryUtil.kt b/documentscanner/src/main/java/com/websitebeaver/documentscanner/utils/GalleryUtil.kt new file mode 100644 index 0000000..35a97c5 --- /dev/null +++ b/documentscanner/src/main/java/com/websitebeaver/documentscanner/utils/GalleryUtil.kt @@ -0,0 +1,88 @@ +package com.websitebeaver.documentscanner.utils + +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import com.websitebeaver.documentscanner.extensions.saveToFile +import java.io.IOException +import java.io.File + +/** + * This class contains a helper function for opening the gallery. + * + * @param activity current activity + * @param onGallerySuccess gets called with photo file path when photo is ready + * @param onCancelGallery gets called when user cancels out of gallery + * @constructor creates gallery util + */ +class GalleryUtil( + private val activity: ComponentActivity, + private val onGallerySuccess: (photoFilePath: String) -> Unit, + private val onCancelGallery: () -> Unit +) { + /** @property photoFilePath the photo file path */ + private lateinit var photoFilePath: String + + /** @property photoFile the photo file */ + private lateinit var photoFile: File + + /** @property startForResult used to launch gallery */ + private val startForResult = + activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + result: ActivityResult -> + when (result.resultCode) { + Activity.RESULT_OK -> { + val data = result.data + val uri = data?.data!! + + // create bitmap from image selection and save it to file + ImageUtil().readBitmapFromFileUriString( + uri.toString(), + activity.contentResolver + ).saveToFile(photoFile, 100) + + // send back photo file path on success + onGallerySuccess(photoFilePath) + } + Activity.RESULT_CANCELED -> { + // delete the photo since the user didn't finish choosing the photo + File(photoFilePath).delete() + onCancelGallery() + } + } + } + + /** + * open the gallery by launching an open document intent + * + * @param pageNumber the current document page number + */ + @Throws(IOException::class) + fun openGallery(pageNumber: Int) { + // create intent to open gallery + val openGalleryIntent = getGalleryIntent() + + // create new file for photo + photoFile = FileUtil().createImageFile(activity, pageNumber) + + // store the photo file path, and send it back once the photo is saved + photoFilePath = photoFile.absolutePath + + // open gallery + startForResult.launch(openGalleryIntent) + } + + private fun getGalleryIntent(): Intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + type = "image/*" + addCategory(Intent.CATEGORY_OPENABLE) + addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } + +} \ No newline at end of file