Android cropping image from camera or gallery

Recently I needed to implement avatar image upload from an Android app, I didn't found a library that did all that I needed so I forked a pretty good one and made it better, check it out: Android Image Cropper.
 
See also the followup post: Android Image Cropper async support and custom progress UI.
 

Requirements:

  • Pick image from camera or gallery using single chooser.
  • Select circular crop window in the image for the avatar.
  • Limit output avatar image to 500×500 pixels.
  • Efficient memory usage.


 

Obstacles:

  • Creating single chooser intent for camera and gallery is not so trivial.
  • Android crop activity is limited, unreliable and is internal API.
  • Image taken by camera may be rotated.
  • Picked image can be large and cause memory issues if loaded in full resolution.

 
After trying cropper library, I decide to fork it, add circular crop support option inspired by another fork, fix some of the reported bugs and most importantly add support for loading image from Android URI that received from camera or gallery activity result.

  • Same code can be used for camera and gallery source.
  • Automatically rotate the image by exif data.
  • Use sampling and lower density to conserve memory usage.
  • When cropped image is requested it will again use sampling of the original image to create bitmap of the required size without loading the full image or lowering the quality of the cropped image.

 

Implementation

Check out the Gists or see the code below on how to create image source chooser Intent and use Android Image Cropper library to crop the picked image.

  1. Create Intent using getPickImageChooserIntent() that will allow to choose camera or gallery sources.
    1. Use predefined URI from getCaptureImageOutputUri() for camera source.
  2. Start activity by intent chooser.

  3. On activity result retrieve the picked image URI by getPickImageResultUri()

    1. For camera source use the predefined URI from getCaptureImageOutputUri()
    2. For gallery source use Intent.getData()
  4. Set the image into CropImageView using setImageUriAsync(Uri uri)
    1. The image will be loaded using ContentResolver
    2. The image will be sampled to the size of the device screen with lower density.
  5. When the user finishes with cropping he/she will click on "crop" button.

  6. Retrieve the cropped image using getCroppedImage(500, 500) .
    1. Will use BitmapRegionDecoder to load only the cropped region
    2. The image will be sampled to the requested size.

 
clip_image001.jpg
 

Code

Add write external storage permissions to the manifest as some sources (DropBox, OneDrive, Etc.) use external storage to return the image resulting in “java.io.FileNotFoundException … open failed: EACCES (Permission denied)”.

<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

 
See it on Gists.

public class  MainActivity extends ActionBarActivity {      

private CropImageView mCropImageView;      
private Uri mCropImageUri;

@Override      
    protected void onCreate(Bundle  savedInstanceState) {      
        super.onCreate(savedInstanceState);      
        setContentView(R.layout.activity_main);      
        mCropImageView = (CropImageView)  findViewById(R.id.CropImageView);      
    }      

/**      
     * On load image button click, start pick  image chooser activity.      
     */      
    public void onLoadImageClick(View view) {      
        startActivityForResult(getPickImageChooserIntent(), 200);      
    }      

/**      
     * Crop the image and set it back to the  cropping view.      
     */      
    public void onCropImageClick(View view) {      
        Bitmap cropped =  mCropImageView.getCroppedImage(500, 500);      
        if (cropped != null)      
            mCropImageView.setImageBitmap(cropped);      
    }      

@Override      
    protected void onActivityResult(int  requestCode, int resultCode, Intent data) {      
        if (resultCode == Activity.RESULT_OK) {      
            Uri imageUri =  getPickImageResultUri(data);     

            // For API >= 23 we need to check specifically that we have permissions to read external storage,
            // but we don't know if we need to for the URI so the simplest is to try open the stream and see if we get error.
            boolean requirePermissions = false;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
                    checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED &&
                    isUriRequiresPermissions(imageUri)) {

                // request permissions and handle the result in onRequestPermissionsResult()
                requirePermissions = true;
                mCropImageUri = imageUri;
                requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
            }

            if (!requirePermissions) {
                mCropImageView.setImageUriAsync(imageUri);
            }
        }      
    }      

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        if (mCropImageUri != null && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            mCropImageView.setImageUriAsync(mCropImageUri);
        } else {
            Toast.makeText(this, "Required permissions are not granted", Toast.LENGTH_LONG).show();
        }
    }

/**      
     * Create a chooser intent to select the  source to get image from.<br/>      
     * The source can be camera's  (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).<br/>      
     * All possible sources are added to the  intent chooser.      
     */      
    public Intent getPickImageChooserIntent() {      

// Determine Uri of camera image to  save.      
        Uri outputFileUri =  getCaptureImageOutputUri();      

List<Intent> allIntents = new  ArrayList<>();      
        PackageManager packageManager =  getPackageManager();      

// collect all camera intents      
        Intent captureIntent = new  Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);      
        List<ResolveInfo> listCam =  packageManager.queryIntentActivities(captureIntent, 0);      
        for (ResolveInfo res : listCam) {      
            Intent intent = new  Intent(captureIntent);      
            intent.setComponent(new  ComponentName(res.activityInfo.packageName, res.activityInfo.name));      
            intent.setPackage(res.activityInfo.packageName);      
            if (outputFileUri != null) {      
                intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);      
            }      
            allIntents.add(intent);      
        }      

// collect all gallery intents      
        Intent galleryIntent = new  Intent(Intent.ACTION_GET_CONTENT);      
        galleryIntent.setType("image/*");      
        List<ResolveInfo> listGallery =  packageManager.queryIntentActivities(galleryIntent, 0);      
        for (ResolveInfo res : listGallery) {      
            Intent intent = new  Intent(galleryIntent);      
            intent.setComponent(new  ComponentName(res.activityInfo.packageName, res.activityInfo.name));      
            intent.setPackage(res.activityInfo.packageName);      
            allIntents.add(intent);      
        }      

// the main intent is the last in the  list (fucking android) so pickup the useless one      
        Intent mainIntent =  allIntents.get(allIntents.size() - 1);      
        for (Intent intent : allIntents) {      
            if  (intent.getComponent().getClassName().equals("com.android.documentsui.DocumentsActivity"))  {      
                mainIntent = intent;      
                break;      
            }      
        }      
        allIntents.remove(mainIntent);      

// Create a chooser from the main  intent      
        Intent chooserIntent =  Intent.createChooser(mainIntent, "Select source");      

// Add all other intents      
        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,  allIntents.toArray(new Parcelable[allIntents.size()]));      

return chooserIntent;      
    }      

/**      
     * Get URI to image received from capture  by camera.      
     */      
    private Uri getCaptureImageOutputUri() {      
        Uri outputFileUri = null;      
        File getImage = getExternalCacheDir();      
        if (getImage != null) {      
            outputFileUri = Uri.fromFile(new  File(getImage.getPath(), "pickImageResult.jpeg"));      
        }      
        return outputFileUri;      
    }      

/**      
     * Get the URI of the selected image from  {@link #getPickImageChooserIntent()}.<br/>      
     * Will return the correct URI for camera  and gallery image.      
     *      
     * @param data the returned data of the  activity result      
     */      
    public Uri getPickImageResultUri(Intent  data) {      
        boolean isCamera = true;      
        if (data != null && data.getData() != null) {
            String action = data.getAction();      
            isCamera = action != null  && action.equals(MediaStore.ACTION_IMAGE_CAPTURE);      
        }      
        return isCamera ?  getCaptureImageOutputUri() : data.getData();      
    }      

    /**
     * Test if we can open the given Android URI to test if permission required error is thrown.<br>
     */
    public boolean isUriRequiresPermissions(Uri uri) {
        try {
            ContentResolver resolver = getContentResolver();
            InputStream stream = resolver.openInputStream(uri);
            stream.close();
            return false;
        } catch (FileNotFoundException e) {
            if (e.getCause() instanceof ErrnoException) {
                return true;
            }
        } catch (Exception e) {
        }
        return false;
    }
}      

 

<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"      
              xmlns:app="http://schemas.android.com/apk/res-auto"      
              android:layout_width="match_parent"      
              android:layout_height="match_parent"      
              android:orientation="vertical">      

<Button      
        android:layout_width="match_parent"      
        android:layout_height="wrap_content"      
        android:padding="@dimen/activity_horizontal_margin"      
        android:text="Load Image"      
        android:onClick="onLoadImageClick"/>      

<com.theartofdev.edmodo.cropper.CropImageView      
        android:id="@+id/CropImageView"      
        android:layout_width="match_parent"      
        android:layout_height="0dp"      
        android:layout_weight="1"      
        app:scaleType="fitCenter"/>      

<Button      
        android:layout_width="match_parent"      
        android:layout_height="wrap_content"      
        android:padding="@dimen/activity_horizontal_margin"      
        android:text="Crop Image"      
        android:onClick="onCropImageClick"/>      

</LinearLayout>      
Advertisement

17 comments on “Android cropping image from camera or gallery

  1. michele says:

    It crash when the cropped image size is too small

  2. Gabriel says:

    How can i use a square cropShape?

  3. tenosca says:

    I just tweeted you asking for your email but i guess we can talk from here, before I can go far I would like to thank you for your great work, I like your Library, but I wanted instead of creating a photo cropper to a view resizer, like an imageview resizer, drag to resize like we resize the rectangle in the cropper window, I have not been very successful, although I do not get any crashes, I get wrong side readings, I really need your help on this, if you could email me i would very much appreciate, fro there I can send you my work based on you owesome cropper, maybe even await release of your view resizer based on what I have done, thank you again

    • Arthur says:

      Hey, my work on this project was relatively minor, most of the hard work was done by edmodo.
      Also, I’ve started a startup firm, the reason for this blog to be silent for the past months and me not working on Android for more than 6 months.
      So, sorry I won’t be able to help you there.
      Good luck.

  4. […] embarrassing really, soon after creating my Android-Image-Cropper fork as I wrote about in my previous post, a GitHub user noted that I have a slowness issue caused by loading/decoding image URI on main […]

  5. imran says:

    HI Team,

    I want to crop my image using my finger in any shape irrespective of any shape of cropping..

    Can I get the code

    Thanks & Regards
    Imran Mohammad

  6. santosh says:

    I tried to use this code… and not able to hide crop after selecting button

  7. shubhandroid says:

    I tried a lot to set fixed size rectangle shape but was unable to do that, Please let me know How to set fixed size rectangular cropping box?

  8. Vaibhav says:

    When I cropped image using this library the image stores on device. It should not stored image in device.

    • Arthur says:

      You have options:
      1. Get only the cropping data.
      2. Not to use the activity but CropImageView directly, then you can get the bitmap object of the cropped image.
      But passing the bitmap data between activities is a no-go.

      Note, GitHub issues page is better platform for questions.

      cheers.

  9. Newb says:

    I didn’t understand this topic in the wiki, and as a newbie this took me longer than i’m willing to admit to figure this out, but in a different way, involving more complexity and lots of bugs. So almost after a week, I finally managed to finish this part of the code, on my own, but with some minor bugs. But then looking about your projects, etc re-read the README and found this link, with a much more elegant way of doing it, to not feel so bad, at least I can say that I learned a lot in this time. Anyways, added this link to the wiki just in case there is some newbie who doesn’t pay too much attention like me 🙂

    And thank you for this great library, you are awesome.

    Cheers.

  10. DrMad says:

    Dude you rock ! I’ve just used your ImageCropper lib. It is very well done, very well documented and does what I expected it to do. This is a rare thing. Thanx so much for what you’ve done (saved me ALOT of time !) and keep the great work up !

Leave a Reply to Arthur Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s