How to crop image in Android

Crop image via rectangle or circle shape

In this section I'll show how to crop image via rectangle shape using Android-Image-Cropper and image from camera.

To get picture from Camera and write access to disk I'm going to use EasyPermissions.

Add the below line in your module’s build.gradle file:

dependencies {
    ...
    implementation 'pub.devrel:easypermissions:1.2.0'
    implementation 'com.theartofdev.edmodo:android-image-cropper:2.7.0'
}

Add permissions and CropImageActivity into your AndroidManifest.xml

<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application ..>
    <activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
        android:theme="@style/Base.Theme.AppCompat"/>
</application>

Following is layout for MainActivity.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn"
        android:onClick="take"
        android:text="Take"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ImageView
        android:id="@+id/iv"
        android:layout_below="@id/btn"
        android:layout_centerHorizontal="true"
        android:layout_width="400dp"
        android:layout_height="400dp" />

</RelativeLayout>

Following is MainActivity.

public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks  {
    private ImageView iv;
    private File file;
    private Uri uri;
    private Activity activity = MainActivity.this;
    private String TAG = MainActivity.class.getSimpleName();
    private String PERM_RATIONALE = "This app needs access to your camera.";
    private static final int RC_SETTINGS = 126;
    private static final int RC_PERM_CAMERA_STORAGE = 127;
    private static final int CAMERA_TAKE_REQUEST = 200;

    private String[] wantedPerms = {Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        iv = findViewById(R.id.iv);

        if (!EasyPermissions.hasPermissions(activity, wantedPerms)) {
            EasyPermissions.requestPermissions(activity, PERM_RATIONALE, 
                RC_PERM_CAMERA_STORAGE, wantedPerms);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch(requestCode){
            case CAMERA_TAKE_REQUEST:
                CropImage.activity(android.net.Uri.parse(file.toURI().toString()))
                        .setAspectRatio(1,1)
                        .setFixAspectRatio(true)
                        .start(activity);
                break;
            case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE:
                CropImage.ActivityResult result = CropImage.getActivityResult(data);
                if (resultCode == RESULT_OK) {
                    iv.setImageURI(result.getUri());
                } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
                    Exception error = result.getError();
                    Log.d(TAG, "onActivityResult: " + error.getMessage());
                }
                break;
        }
    }

    //
    // Permissions
    //

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        // Forward results to EasyPermissions
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }

    @Override
    public void onPermissionsGranted(int requestCode, List perms) {
        Log.d(TAG, "onPermissionsGranted:" + requestCode + ":" + perms.size());
    }

    @Override
    public void onPermissionsDenied(int requestCode, List perms) {
        Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());
        if (EasyPermissions.somePermissionPermanentlyDenied(activity, perms)) {
            new AppSettingsDialog.Builder(activity)
                    .setTitle("Permissions Required")
                    .setPositiveButton("Settings")
                    .setNegativeButton("Cancel")
                    .setRequestCode(RC_SETTINGS)
                    .build()
                    .show();
        }
    }

    @AfterPermissionGranted(RC_PERM_CAMERA_STORAGE)
    private void afterCameraStoragePermission() {
        if (EasyPermissions.hasPermissions(this, wantedPerms)) {
            Log.d(TAG, "Already have permission, do the thing");
        } else {
            Log.d(TAG, "Do not have permission, request them now");
            EasyPermissions.requestPermissions(activity, PERM_RATIONALE, 
                RC_PERM_CAMERA_STORAGE, wantedPerms);
        }
    }

    //
    // Camera
    //

    @TargetApi(Build.VERSION_CODES.M)
    public void take(View v) {
        if(checkCameraExists()) {
            if (EasyPermissions.hasPermissions(activity, wantedPerms)) {
                launchCamera();
            } else {
                EasyPermissions.requestPermissions(activity, PERM_RATIONALE, 
                   RC_PERM_CAMERA_STORAGE, wantedPerms);
            }
        } else {
            Toast.makeText(activity, "Camera not available.", Toast.LENGTH_SHORT).show();
        }
    }

    public boolean checkCameraExists() {
        return activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
    }

    private void launchCamera() {
        Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);

        file = new File(Environment.getExternalStorageDirectory(), 
            String.valueOf(System.currentTimeMillis()) + ".jpg");
        uri = FileProvider.getUriForFile(activity, 
            activity.getApplicationContext().getPackageName() + ".provider", file);

        intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, uri);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

        startActivityForResult(intent, CAMERA_TAKE_REQUEST);
    }
}

Android 7.0 Nougat introduced some file system permission changes in order to improve security. If you’ve already updated your app to targetSdkVersion 24 (or higher) and you’re passing a file:// URI outside your package domain through an Intent, then what you’ll get is a FileUriExposedException.

So, Android may throw FileUriExposedException in Android 7.0 (API level 24) and above, this exception will come when you will expose a file:// URIs outside your package domain through Intent.

FileProvider is a special subclass of ContentProvider which allows us to securely share file through a content:// URI instead of file:// one. Why is this a better approach? Because you’re granting a temporary access to the file, which will be available for the receiver activity or service until they are active/running.

We create our own class inheriting FileProvider in order to make sure our FileProvider doesn't conflict with FileProviders declared in imported dependencies as described here.

Add a class extending FileProvider

public class GenericFileProvider extends FileProvider {}

Next, add the GenericFileProvider in our AndroidManifest.xml:

<manifest>
    ...
    <application>
        ...
        <provider
            android:name=".GenericFileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_provider_paths" />
        </provider>
        ...
    </application>
</manifest>

We’re going to set android:exported to false because we don’t need it to be public, android:grantUriPermissions to true because it will grant temporary access to files and android:authorities to a domain you control, so if your domain is me.proft.superapp then you can use something like me.proft.superapp.provider. The authority of a provider should be unique and that’s the reason why we are using our application ID plus something like .provider.

Then we need to create the file_provider_path in the res/xml folder. That’s the file which defines the folders which contain the files you will be allowed to share safely. In our case we just need access to the external storage folder:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path name="external_files" path="." />
</paths>

The final step is to change the line of code below in

File file = new File(Environment.getExternalStorageDirectory(), 
    String.valueOf(System.currentTimeMillis()) + ".jpg");

Uri photoURI = Uri.fromFile(file);

to

Uri photoURI = FileProvider.getUriForFile(context, 
    context.getApplicationContext().getPackageName() + ".provider", file);

If you're using an Intent to make the system open your file, you may need to add the following line of code:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

Instead of using Uri.fromFile(file) we create our URI with FileProvider.getUriForFile(context, string, file) which will generate a new content:// URI with the authority defined pointing to the file we passed in.

Result

android_crop_rect_shape.png

Other useful libs for image crop:

Crop image via custom shape from bitmap

Sometimes you want to crop a bitmap, but it’s not an ordinary cropping, it’s a cropping to get rounded corners, cropping to get it as a circle, cropping to get it as a star, or cropping to get it as any shape.

In this post, we will crop a heart from an Android bitmap.

At first get a bitmap to crop a shape from it.

Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.landscape);

Create an empty and mutable bitmap with the same height and width of the source.

Bitmap output = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Bitmap.Config.ARGB_8888);

Create a canvas with the mutable bitmap to draw into.

Canvas canvas = new Canvas(output);

Create a paint with any solid color, this color is for drawing a heart which you want to crop from the bitmap.

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(0XFF000000);

Draw a heart path at the center of the canvas.

canvas.drawPath(getPath(src), paint);

In our case, the heart shape which you have drawn is called the destination image, it’s the shape which you want to crop from the bitmap.

The magic will be in the next line of code, set the transfer mode which defines how source pixels are composited or merged with the destination pixels.

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

Draw the source image on the canvas which has the destination image and use the paint with the SRC_IN transformation mode.

canvas.drawBitmap(src, 0, 0, paint);

Following is MainActivity class.

public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks  {
    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        iv = findViewById(R.id.iv);

        Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.landscape);
        Bitmap output = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(output);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(0XFF000000);
        canvas.drawPath(getPath(src), paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(src, 0, 0, paint);

        iv.setImageBitmap(output);
    }

    private Path getPath(Bitmap src) {
        return resizePath(PathParser.createPathFromPathData(getString(R.string.heart)),
                src.getWidth(), src.getHeight());
    }

    private Path resizePath(Path path, float width, float height) {
        RectF bounds = new RectF(0, 0, width, height);
        Path resizedPath = new Path(path);
        RectF src = new RectF();
        resizedPath.computeBounds(src, true);

        Matrix resizeMatrix = new Matrix();
        resizeMatrix.setRectToRect(src, bounds, Matrix.ScaleToFit.CENTER);
        resizedPath.transform(resizeMatrix);

        return resizedPath;
    }
}

Following is strings.xml.

<resources>
    <string name="heart">
    M25.119,2.564c12.434,0.023,18.68,5.892,24.88,17.612  c6.2-11.721,12.446-
17.589,24.877-17.612c13.81-0.025,25.035,10.575,25.061,23.66c0.033,23.708-24.952,
47.46-49.938,71.212  C25.016,73.685,0.03,49.932,0.064,26.224C0.085,
13.14,11.309,2.539,25.119,2.564z
  </string>
</resources>

You can get PathParser here.

Result

android_crop_custom_shape.png