Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

intent option to pick images from gallery #22

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -58,6 +60,10 @@ class DocumentScanner(
DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS,
maxNumDocuments
)
documentScanIntent.putExtra(
DocumentScannerExtra.EXTRA_IMAGE_PROVIDER,
imageProvider
)

return documentScanIntent
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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}"
Expand All @@ -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}")
}
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -318,7 +429,11 @@ class DocumentScannerActivity : AppCompatActivity() {
*/
private fun onClickNew() {
addSelectedCornersAndOriginalPhotoPathToDocuments()
openCamera()
if (imageProvider == ImageProvider.CAMERA) {
openCamera()
} else if (imageProvider == ImageProvider.GALLERY) {
openGallery()
}
}

/**
Expand All @@ -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()
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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)
}

}