Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Crop Image from Camera on Android

4.79/5 (12 votes)
3 Feb 2019CPOL2 min read 35.5K   1.8K  
This  snippet allows you crop image with preview of camera on Android devices

Introduction

The basic ideas are very simple:

  • Show camera preview
  • Add rectangle view to camera preview (boundaries of new crop image)
  • Crop image and save result to file

Using the Code

Let's start! Create a new project in Android studio (I used version 3.2.1) or you can download the source files and choose: File-New-Import project. Add to build.gradle app level:

Java
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

I use Butterknife library, it's very useful. Also, we need camera and write permissions, so add it to AndroidManifest.xml.

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

We need 2 fragments, one for camera preview and one to show cropped image. Create new fragment -ImageFragment (File-New-Fragment-Fragment(blank) -add TextView and Imageview to layout-xml file:

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

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/res_photo_size"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <ImageView
            android:id="@+id/res_photo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_launcher_background" />

    </LinearLayout>
</FrameLayout>

Image 1

Add some code to ImageFragment class, it simply show image and textview information:

Java
public class ImageFragment extends Fragment {

    private Bitmap bitmap;

    @BindView(R.id.res_photo)
    ImageView resPhoto;

    @BindView(R.id.res_photo_size)
    TextView resPhotoSize;

    public void imageSetupFragment(Bitmap bitmap) {
        if (bitmap != null) {
            this.bitmap = bitmap;
        }
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setRetainInstance(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment_image, container, false);
        ButterKnife.bind(this, view);
        //check if bitmap exist, set to ImageView
        if (bitmap != null) {
            resPhoto.setImageBitmap(bitmap);
            String info = "image with:" + bitmap.getWidth() + "\n" + 
                          "image height:" + bitmap.getHeight();
            resPhotoSize.setText(info);
        }
        return view;
    }
}

Create second fragment -PhotoFragment (File-New-Fragment-Fragment(blank) - add some components to layout-xml file. Main is SurfaceView (for camera preview) and View (border for cropping).

XML
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/frame_surface_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:id="@+id/preview_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <SurfaceView
                android:id="@+id/camera_preview_surface"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_centerInParent="true" />

            <View
                android:id="@+id/border_camera"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:layout_centerInParent="true"
                android:layout_marginStart="50dp"
                android:layout_marginEnd="50dp"
                android:background="@drawable/border" />
            <!---Customize your views and button-->
            <TextView
                android:id="@+id/res_border_size"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentStart="true"
                android:layout_alignParentBottom="true"
                android:text="size"
                android:textColor="@color/colorAccent" />

            <Button
                android:id="@+id/make_photo_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentEnd="true"
                android:layout_alignParentBottom="true"
                android:background="@drawable/photo_button"
                android:text="photo" />
        </RelativeLayout>
    </LinearLayout>
</FrameLayout>

Image 2

I used a custom button (green button), it's simple code but great view! To do this, you need to create a new XML file in Res-drawable- like photo_button.xml, set up shape, colors as you need or use some web resources, like this: http://angrytools.com/android/button/.

XML
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners
        android:bottomLeftRadius="0dp"
        android:bottomRightRadius="20dp"
        android:topLeftRadius="20dp"
        android:topRightRadius="0dp" />
    <gradient
        android:angle="45"
        android:centerColor="#47A891"
        android:centerX="35%"
        android:endColor="#000000"
        android:startColor="#E8E8E8"
        android:type="linear" />
    <size
        android:width="100dp"
        android:height="60dp" />
    <stroke
        android:width="3dp"
        android:color="#16875A" />
</shape>

and then set background button to this:

Java
android:background="@drawable/photo_button"

Image 3

Also, we need crop border - it will be a simple rectangle, to do this, you need to create a new XML file in Res-drawable- like border.xml:

XML
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <stroke android:width="2dp" android:color="#C4CDD5"  />
    <corners android:radius="10dp" />
    <padding android:left="2dp" android:top="2dp" 
    android:right="2dp" android:bottom="2dp" />
</shape>

and set view-background to:

Java
android:background="@drawable/border" />

Move next, PhotoFragment class. We can't use standard intent to make a photo, we need custom functions, so we can use Camera class - it's deprecated, but still works nice, so let's use it. The Camera class is used to set image capture settings, start/stop preview, snap pictures, and retrieve frames for encoding for video. This class is a client for the Camera service, which manages the actual camera hardware.

To control preview, we need to use SurfaceHolder.Callback. This abstract interface is to hold a display surface. Allows you to control the surface size and format, edit the pixels in the surface, and monitor changes to the surface.

Java
public class PhotoFragment extends Fragment implements SurfaceHolder.Callback
{}

and implements some methods:

Java
@Override
public void surfaceCreated(SurfaceHolder holder) {

}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {

}

Like this:

Java
@Override
public void surfaceCreated(SurfaceHolder holder) {
    camera = Camera.open();
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    if (previewing) {
        camera.stopPreview();
        previewing = false;
    }

    if (camera != null) {
        try {
            Camera.Parameters parameters = camera.getParameters();
            //get preview sizes
            List<Camera.Size> previewSizes = parameters.getSupportedPreviewSizes();

            //find optimal - it very important
            previewSizeOptimal = getOptimalPreviewSize(previewSizes, parameters.getPictureSize().width,
                    parameters.getPictureSize().height);

            //set parameters
            if (previewSizeOptimal != null) {
                parameters.setPreviewSize(previewSizeOptimal.width, previewSizeOptimal.height);
            }

            if (camera.getParameters().getFocusMode().contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
                parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
            }
            if (camera.getParameters().getFlashMode().contains(Camera.Parameters.FLASH_MODE_AUTO)) {
                parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
            }

            camera.setParameters(parameters);

            //rotate screen, because camera sensor usually in landscape mode
            Display display = ((WindowManager) context.getSystemService
                                  (Context.WINDOW_SERVICE)).getDefaultDisplay();
            if (display.getRotation() == Surface.ROTATION_0) {
                camera.setDisplayOrientation(90);
            } else if (display.getRotation() == Surface.ROTATION_270) {
                camera.setDisplayOrientation(180);
            }

            //write some info
            int x1 = previewLayout.getWidth();
            int y1 = previewLayout.getHeight();

            int x2 = borderCamera.getWidth();
            int y2 = borderCamera.getHeight();

            String info = "Preview width:" + String.valueOf(x1) + "\n" + 
                                 "Preview height:" + String.valueOf(y1) + "\n" +
                    "Border width:" + String.valueOf(x2) + 
                                 "\n" + "Border height:" + String.valueOf(y2);
            resBorderSizeTV.setText(info);

            camera.setPreviewDisplay(surfaceHolder);
            camera.startPreview();
            previewing = true;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    camera.stopPreview();
    camera.release();
    camera = null;
    previewing = false;
}

Very important to set correct size of camera preview, if aspect ratio of image doesn't fit to preview camera size - cropped image will have incorrect size.

Java
public Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
    final double ASPECT_TOLERANCE = 0.1;
    double targetRatio = (double) w / h;
    if (sizes == null) return null;

    Camera.Size optimalSize = null;
    double minDiff = Double.MAX_VALUE;

    int targetHeight = h;

    // Try to find an size match aspect ratio and size
    for (Camera.Size size : sizes) {
        double ratio = (double) size.width / size.height;
        if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
        if (Math.abs(size.height - targetHeight) < minDiff) {
            optimalSize = size;
            minDiff = Math.abs(size.height - targetHeight);
        }
    }

    // Cannot find the one match the aspect ratio, ignore the requirement
    if (optimalSize == null) {
        minDiff = Double.MAX_VALUE;
        for (Camera.Size size : sizes) {
            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }
    }
    return optimalSize;
}

Define some functions to take picture:

Java
@OnClick(R.id.make_photo_button)
void makePhoto() {
    if (camera != null) {
        camera.takePicture(myShutterCallback,
                myPictureCallback_RAW, myPictureCallback_JPG);
    }
}

and some callbacks:

Java
Camera.ShutterCallback myShutterCallback = new Camera.ShutterCallback() {
    @Override
    public void onShutter() {

    }
};

//leave it empty
Camera.PictureCallback myPictureCallback_RAW = new Camera.PictureCallback() {
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {

    }
};

//we need only JPG
Camera.PictureCallback myPictureCallback_JPG = new Camera.PictureCallback() {
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
        Bitmap bitmapPicture
                = BitmapFactory.decodeByteArray(data, 0, data.length);

        Bitmap croppedBitmap = null;

        Display display = ((WindowManager) context.getSystemService
                              (Context.WINDOW_SERVICE)).getDefaultDisplay();
        if (display.getRotation() == Surface.ROTATION_0) {

            //rotate bitmap, because camera sensor usually in landscape mode
            Matrix matrix = new Matrix();
            matrix.postRotate(90);
            Bitmap rotatedBitmap = Bitmap.createBitmap(bitmapPicture, 0, 0, 
                       bitmapPicture.getWidth(), bitmapPicture.getHeight(), matrix, true);
            //save file
            createImageFile(rotatedBitmap);

            //calculate aspect ratio
            float koefX = (float) rotatedBitmap.getWidth() / (float) previewLayout.getWidth();
            float koefY = (float) rotatedBitmap.getHeight() / previewLayout.getHeight();

            //get viewfinder border size and position on the screen
            int x1 = borderCamera.getLeft();
            int y1 = borderCamera.getTop();

            int x2 = borderCamera.getWidth();
            int y2 = borderCamera.getHeight();

            //calculate position and size for cropping
            int cropStartX = Math.round(x1 * koefX);
            int cropStartY = Math.round(y1 * koefY);

            int cropWidthX = Math.round(x2 * koefX);
            int cropHeightY = Math.round(y2 * koefY);

            //check limits and make crop
            if (cropStartX + cropWidthX <= rotatedBitmap.getWidth() && 
                         cropStartY + cropHeightY <= rotatedBitmap.getHeight()) {
                croppedBitmap = Bitmap.createBitmap(rotatedBitmap, cropStartX, 
                                                    cropStartY, cropWidthX, cropHeightY);
            } else {
                croppedBitmap = null;
            }

            //save result
            if (croppedBitmap != null) {
                createImageFile(croppedBitmap);
            }

        } else if (display.getRotation() == Surface.ROTATION_270) {
            // for Landscape mode
        }

        //pass to another fragment
        if (mListener != null) {
            if (croppedBitmap != null)
                mListener.onFragmentInteraction(croppedBitmap);
        }

        if (camera != null) {
            camera.startPreview();
        }
    }
};

Calculation to crop image is simple:

Java
//calculate aspect ratio
float koefX = (float) rotatedBitmap.getWidth() / (float) previewLayout.getWidth();
float koefY = (float) rotatedBitmap.getHeight() / (float)previewLayout.getHeight();

//get viewfinder border size and position on the screen
int x1 = borderCamera.getLeft();
int y1 = borderCamera.getTop();

int x2 = borderCamera.getWidth();
int y2 = borderCamera.getHeight();

//calculate position and size for cropping
int cropStartX = Math.round(x1 * koefX);
int cropStartY = Math.round(y1 * koefY);

int cropWidthX = Math.round(x2 * koefX);
int cropHeightY = Math.round(y2 * koefY);

//check limits and make crop
if (cropStartX + cropWidthX <= rotatedBitmap.getWidth() && cropStartY + 
                                 cropHeightY <= rotatedBitmap.getHeight()) {
    croppedBitmap = Bitmap.createBitmap
                (rotatedBitmap, cropStartX, cropStartY, cropWidthX, cropHeightY);
} else {
    croppedBitmap = null;
}

Also, we need write bitmap to file:

Java
public void createImageFile(final Bitmap bitmap) {

    File path = Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES);

    String timeStamp = new SimpleDateFormat("MMdd_HHmmssSSS").format(new Date());
    String imageFileName = "region_" + timeStamp + ".jpg";
    final File file = new File(path, imageFileName);

    try {
        // Make sure the Pictures directory exists.
        if (path.mkdirs()) {
            Toast.makeText(context, "Not exist :" + path.getName(), Toast.LENGTH_SHORT).show();
        }

        OutputStream os = new FileOutputStream(file);

        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);

        os.flush();
        os.close();
        Log.i("ExternalStorage", "Writed " + path + file.getName());
        // Tell the media scanner about the new file so that it is
        // immediately available to the user.
        MediaScannerConnection.scanFile(context,
                new String[]{file.toString()}, null,
                new MediaScannerConnection.OnScanCompletedListener() {
                    public void onScanCompleted(String path, Uri uri) {
                        Log.i("ExternalStorage", "Scanned " + path + ":");
                        Log.i("ExternalStorage", "-> uri=" + uri);
                    }
                });
        Toast.makeText(context, file.getName(), Toast.LENGTH_SHORT).show();

    } catch (Exception e) {
        // Unable to create file, likely because external storage is
        // not currently mounted.
        Log.w("ExternalStorage", "Error writing " + file, e);
    }
}

Design MainActivity class:

XML
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <Button
                android:id="@+id/make_photo_button"
                android:layout_gravity="center"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Photo!" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/res_photo_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

        </LinearLayout>
    </LinearLayout>
</FrameLayout>

Image 4

and put some code:

Java
public class MainActivity extends AppCompatActivity 
       implements PhotoFragment.OnFragmentInteractionListener {

    int PERMISSION_ALL = 1;
    boolean flagPermissions = false;

    String[] PERMISSIONS = {
            android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
            android.Manifest.permission.CAMERA
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        checkPermissions();
    }

    @OnClick(R.id.make_photo_button)
    void onClickScanButton() {
        // check permissions
        if (!flagPermissions) {
            checkPermissions();
            return;
        }
        //start photo fragment
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.res_photo_layout, new PhotoFragment())
                .addToBackStack(null)
                .commit();
    }

    void checkPermissions() {
        if (!hasPermissions(this, PERMISSIONS)) {
            requestPermissions(PERMISSIONS,
                    PERMISSION_ALL);
            flagPermissions = false;
        }
        flagPermissions = true;
    }

    public static boolean hasPermissions(Context context, String... permissions) {
        if (context != null && permissions != null) {
            for (String permission : permissions) {
                if (ActivityCompat.checkSelfPermission(context, permission) 
                        != PackageManager.PERMISSION_GRANTED) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public void onFragmentInteraction(Bitmap bitmap) {
        if (bitmap != null) {
            ImageFragment imageFragment = new ImageFragment();
            imageFragment.imageSetupFragment(bitmap);

            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.res_photo_layout, imageFragment)
                    .addToBackStack(null)
                    .commit();
        }
    }
}

Test of crop:

Image 5

Image 6

Points of Interest

I hope this simple article will help you. You can easily improve this application. I like to develop applications, so you can try some of them on https://play.google.com/store/apps/developer?id=VOLOSHYN+SERGIY.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)