/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.onegravity.rteditor.media.crop; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Path; import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.media.FaceDetector; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.Surface; import android.view.View; import com.onegravity.rteditor.R; import com.onegravity.rteditor.media.MediaUtils; import com.onegravity.rteditor.media.MonitoredActivity; import com.onegravity.rteditor.utils.Helper; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * The activity can crop specific region of interest from an image. */ public class CropImageActivity extends MonitoredActivity { private final float IMAGE_MAX_SIZE = 1024f; public static final String IMAGE_SOURCE_FILE = "image-source-file"; public static final String IMAGE_DESTINATION_FILE = "image-dest-file"; public static final String SCALE = "scale"; public static final String ORIENTATION_IN_DEGREES = "orientation_in_degrees"; public static final String ASPECT_X = "aspectX"; public static final String ASPECT_Y = "aspectY"; public static final String OUTPUT_X = "outputX"; public static final String OUTPUT_Y = "outputY"; public static final String SCALE_UP_IF_NEEDED = "scaleUpIfNeeded"; public static final String CIRCLE_CROP = "circleCrop"; public static final String RETURN_DATA = "return-data"; public static final String RETURN_DATA_AS_BITMAP = "data"; public static final String ACTION_INLINE_DATA = "inline-data"; // These are various options can be specified in the intent. private Bitmap.CompressFormat mOutputFormat = Bitmap.CompressFormat.JPEG; private Uri mSaveUri = null; private boolean mDoFaceDetection = false; private boolean mCircleCrop = false; private int mAspectX; private int mAspectY; private int mOutputX; private int mOutputY; private boolean mScale; private CropImageView mImageView; private Bitmap mBitmap; private String mImageSource; private String mImageDest; boolean mWaitingToPick; // Whether we are wait the user to pick a face. boolean mSaving; // Whether the "save" button is already clicked. HighlightView mCrop; // These options specify the output image size and whether we should // scale the output to fit it (or just crop it). private boolean mScaleUp = true; private final BitmapManager.ThreadSet mDecodingThreads = new BitmapManager.ThreadSet(); // ****************************************** Lifecycle Methods ******************************************* @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.rte_crop_image); mImageView = (CropImageView) findViewById(R.id.image); Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { if (extras.getString(CIRCLE_CROP) != null) { mImageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); mCircleCrop = true; mAspectX = 1; mAspectY = 1; } mImageSource = extras.getString(IMAGE_SOURCE_FILE); mBitmap = getBitmap(mImageSource); mImageDest = extras.getString(IMAGE_DESTINATION_FILE); if (mImageDest == null) { mImageDest = mImageSource; } mSaveUri = Uri.fromFile(new File(mImageDest)); if (extras.containsKey(ASPECT_X) && extras.get(ASPECT_X) instanceof Integer) { mAspectX = extras.getInt(ASPECT_X); } else { throw new IllegalArgumentException("aspect_x must be integer"); } if (extras.containsKey(ASPECT_Y) && extras.get(ASPECT_Y) instanceof Integer) { mAspectY = extras.getInt(ASPECT_Y); } else { throw new IllegalArgumentException("aspect_y must be integer"); } mOutputX = extras.getInt(OUTPUT_X); mOutputY = extras.getInt(OUTPUT_Y); mScale = extras.getBoolean(SCALE, true); mScaleUp = extras.getBoolean(SCALE_UP_IF_NEEDED, true); } if (mBitmap == null) { finish(); return; } startFaceDetection(); } // ****************************************** ActionBar / Option Menu ******************************************* @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.rte_crop_image, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.save) { try { onSave(); } catch (Exception e) { Log.e(getClass().getSimpleName(), e.getMessage(), e); finish(); } return true; } else if (itemId == R.id.cancel) { setResult(RESULT_CANCELED); finish(); return true; } else if (itemId == R.id.rotate_left) { mBitmap = rotateImage(mBitmap, -90); RotateBitmap rotateBitmap = new RotateBitmap(mBitmap); mImageView.setImageRotateBitmapResetBase(rotateBitmap, true); mRunFaceDetection.run(); return true; } else if (itemId == R.id.rotate_right) { mBitmap = rotateImage(mBitmap, 90); RotateBitmap rotateBitmap = new RotateBitmap(mBitmap); mImageView.setImageRotateBitmapResetBase(rotateBitmap, true); mRunFaceDetection.run(); return true; } return super.onOptionsItemSelected(item); } // ****************************************** Private Methods ******************************************* private Bitmap getBitmap(String path) { Uri uri = MediaUtils.createFileUri(path); InputStream in = null; try { in = getContentResolver().openInputStream(uri); // Decode image size BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(in, null, options); Helper.closeQuietly(in); // compute scale factor to ensure that the image is smaller than // IMAGE_MAX_SIZE float maxSize = Math.max(options.outWidth, options.outHeight); float scale = maxSize > IMAGE_MAX_SIZE ? maxSize / IMAGE_MAX_SIZE : 1.0f; while ((maxSize / scale) > 8) { try { return getBitmap(in, uri, scale); } catch (Throwable e) { Log.w(getClass().getSimpleName(), "bitmap could not be created (probably out of memory), decreasing size and retrying"); scale *= 2f; } } } catch (IOException e) { Log.e(getClass().getSimpleName(), "file " + path + " not found"); } catch (Exception e) { Log.e(getClass().getSimpleName(), "error while opening image", e); } finally { Helper.closeQuietly(in); } return null; } private Bitmap getBitmap(InputStream in, Uri uri, float scale) throws Throwable { // decode image BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = false; options.inSampleSize = Math.round(scale); in = getContentResolver().openInputStream(uri); return BitmapFactory.decodeStream(in, null, options); } private void startFaceDetection() { if (isFinishing()) { return; } mImageView.setImageBitmapResetBase(mBitmap, true); startBackgroundJob(R.string.rte_processing_image, new Runnable() { public void run() { final Bitmap b = mBitmap; if (b != mBitmap && b != null) { mImageView.setImageBitmapResetBase(b, true); mBitmap.recycle(); mBitmap = b; } if (mImageView.getScale() == 1F) { mImageView.center(true, true); } mRunFaceDetection.run(); } }); } private void onSave() throws Exception { // TODO this code needs to change to use the decode/crop/encode single // step api so that we don't require that the whole (possibly large) // bitmap doesn't have to be read into memory if (mSaving) return; if (mCrop == null) { return; } mSaving = true; Rect r = mCrop.getCropRect(); int width = r.width(); int height = r.height(); // If we are circle cropping, we want alpha channel, which is the third // param here. Bitmap croppedImage; try { croppedImage = Bitmap.createBitmap(width, height, mCircleCrop ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); } catch (Exception e) { throw e; } if (croppedImage == null) { return; } { Canvas canvas = new Canvas(croppedImage); Rect dstRect = new Rect(0, 0, width, height); canvas.drawBitmap(mBitmap, r, dstRect, null); } if (mCircleCrop) { // OK, so what's all this about? // Bitmaps are inherently rectangular but we want to return // something that's basically a circle. So we fill in the // area around the circle with alpha. Note the all important // PortDuff.Mode.CLEAR. Canvas c = new Canvas(croppedImage); Path p = new Path(); p.addCircle(width / 2F, height / 2F, width / 2F, Path.Direction.CW); c.clipPath(p, Region.Op.DIFFERENCE); c.drawColor(0x00000000, PorterDuff.Mode.CLEAR); } /* If the output is required to a specific size then scale or fill */ if (mOutputX != 0 && mOutputY != 0) { if (mScale) { /* Scale the image to the required dimensions */ Bitmap old = croppedImage; croppedImage = transform(new Matrix(), croppedImage, mOutputX, mOutputY, mScaleUp); if (old != croppedImage) { old.recycle(); } } else { /* * Don't scale the image crop it to the size requested. Create * an new image with the cropped image in the center and the * extra space filled. */ // Don't scale the image but instead fill it so it's the // required dimension Bitmap b = Bitmap.createBitmap(mOutputX, mOutputY, Bitmap.Config.RGB_565); Canvas canvas = new Canvas(b); Rect srcRect = mCrop.getCropRect(); Rect dstRect = new Rect(0, 0, mOutputX, mOutputY); int dx = (srcRect.width() - dstRect.width()) / 2; int dy = (srcRect.height() - dstRect.height()) / 2; /* If the srcRect is too big, use the center part of it. */ srcRect.inset(Math.max(0, dx), Math.max(0, dy)); /* If the dstRect is too big, use the center part of it. */ dstRect.inset(Math.max(0, -dx), Math.max(0, -dy)); /* Draw the cropped bitmap in the center */ canvas.drawBitmap(mBitmap, srcRect, dstRect, null); /* Set the cropped bitmap as the new bitmap */ croppedImage.recycle(); croppedImage = b; } } // Return the cropped image directly or save it to the specified URI. Bundle myExtras = getIntent().getExtras(); if (myExtras != null && (myExtras.getParcelable("data") != null || myExtras.getBoolean(RETURN_DATA))) { Bundle extras = new Bundle(); extras.putParcelable(RETURN_DATA_AS_BITMAP, croppedImage); setResult(RESULT_OK, (new Intent()).setAction(ACTION_INLINE_DATA).putExtras(extras)); finish(); } else { final Bitmap b = croppedImage; startBackgroundJob(R.string.rte_processing_image, new Runnable() { public void run() { saveOutput(b); } }); } } private void saveOutput(Bitmap croppedImage) { if (mSaveUri != null) { OutputStream out = null; try { out = getContentResolver().openOutputStream(mSaveUri); if (out != null) { croppedImage.compress(mOutputFormat, 90, out); } } catch (IOException ex) { Log.e(getClass().getSimpleName(), "Cannot open file: " + mSaveUri, ex); setResult(RESULT_CANCELED); finish(); return; } finally { Helper.closeQuietly(out); } Bundle extras = new Bundle(); Intent intent = new Intent(mSaveUri.toString()); intent.putExtras(extras); intent.putExtra(IMAGE_SOURCE_FILE, mImageSource); intent.putExtra(IMAGE_DESTINATION_FILE, mImageDest); intent.putExtra(ORIENTATION_IN_DEGREES, getOrientationInDegree(this)); setResult(RESULT_OK, intent); } else { Log.e(getClass().getSimpleName(), "not defined image url"); } croppedImage.recycle(); finish(); } @Override protected void onPause() { super.onPause(); BitmapManager.instance().cancelThreadDecoding(mDecodingThreads); } @Override protected void onDestroy() { super.onDestroy(); if (mBitmap != null) { mBitmap.recycle(); } } Runnable mRunFaceDetection = new Runnable() { float mScale = 1F; Matrix mImageMatrix; FaceDetector.Face[] mFaces = new FaceDetector.Face[3]; int mNumFaces; // For each face, we create a HightlightView for it. private void handleFace(FaceDetector.Face f) { PointF midPoint = new PointF(); int r = ((int) (f.eyesDistance() * mScale)) * 2; f.getMidPoint(midPoint); midPoint.x *= mScale; midPoint.y *= mScale; int midX = (int) midPoint.x; int midY = (int) midPoint.y; HighlightView hv = new HighlightView(mImageView); int width = mBitmap.getWidth(); int height = mBitmap.getHeight(); Rect imageRect = new Rect(0, 0, width, height); RectF faceRect = new RectF(midX, midY, midX, midY); faceRect.inset(-r, -r); if (faceRect.left < 0) { faceRect.inset(-faceRect.left, -faceRect.left); } if (faceRect.top < 0) { faceRect.inset(-faceRect.top, -faceRect.top); } if (faceRect.right > imageRect.right) { faceRect.inset(faceRect.right - imageRect.right, faceRect.right - imageRect.right); } if (faceRect.bottom > imageRect.bottom) { faceRect.inset(faceRect.bottom - imageRect.bottom, faceRect.bottom - imageRect.bottom); } hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop, mAspectX != 0 && mAspectY != 0); mImageView.add(hv); } // Create a default HightlightView if we found no face in the picture. private void makeDefault() { HighlightView hv = new HighlightView(mImageView); int width = mBitmap.getWidth(); int height = mBitmap.getHeight(); Rect imageRect = new Rect(0, 0, width, height); // make the default size about 4/5 of the width or height /* * int cropWidth = Math.min(width, height) * 4 / 5; int cropHeight = * cropWidth; */ int cropWidth = width; int cropHeight = height; if (mAspectX != 0 && mAspectY != 0) { if (mAspectX > mAspectY) { cropHeight = cropWidth * mAspectY / mAspectX; } else { cropWidth = cropHeight * mAspectX / mAspectY; } } int x = (width - cropWidth) / 2; int y = (height - cropHeight) / 2; RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight); hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop, mAspectX != 0 && mAspectY != 0); mImageView.mHighlightViews.clear(); // Thong added for rotate mImageView.add(hv); } // Scale the image down for faster face detection. private Bitmap prepareBitmap() { if (mBitmap == null) { return null; } // 256 pixels wide is enough. if (mBitmap.getWidth() > 256) { mScale = 256.0F / mBitmap.getWidth(); } Matrix matrix = new Matrix(); matrix.setScale(mScale, mScale); return Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight(), matrix, true); } public void run() { mImageMatrix = mImageView.getImageMatrix(); Bitmap faceBitmap = prepareBitmap(); mScale = 1.0F / mScale; if (faceBitmap != null && mDoFaceDetection) { FaceDetector detector = new FaceDetector(faceBitmap.getWidth(), faceBitmap.getHeight(), mFaces.length); mNumFaces = detector.findFaces(faceBitmap, mFaces); } if (faceBitmap != null && faceBitmap != mBitmap) { faceBitmap.recycle(); } mHandler.post(new Runnable() { public void run() { mWaitingToPick = mNumFaces > 1; if (mNumFaces > 0) { for (int i = 0; i < mNumFaces; i++) { handleFace(mFaces[i]); } } else { makeDefault(); } mImageView.invalidate(); if (mImageView.mHighlightViews.size() == 1) { mCrop = mImageView.mHighlightViews.get(0); mCrop.setFocus(true); } } }); } }; /* * Compute the sample size as a function of minSideLength and * maxNumOfPixels. minSideLength is used to specify that minimal width or * height of a bitmap. maxNumOfPixels is used to specify the maximal size in * pixels that are tolerable in terms of memory usage. * * The function returns a sample size based on the constraints. Both size * and minSideLength can be passed in as IImage.UNCONSTRAINED, which * indicates no care of the corresponding constraint. The functions prefers * returning a sample size that generates a smaller bitmap, unless * minSideLength = IImage.UNCONSTRAINED. */ private Bitmap transform(Matrix scaler, Bitmap source, int targetWidth, int targetHeight, boolean scaleUp) { int deltaX = source.getWidth() - targetWidth; int deltaY = source.getHeight() - targetHeight; if (!scaleUp && (deltaX < 0 || deltaY < 0)) { /* * In this case the bitmap is smaller, at least in one dimension, * than the target. Transform it by placing as much of the image as * possible into the target and leaving the top/bottom or left/right * (or both) black. */ Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b2); int deltaXHalf = Math.max(0, deltaX / 2); int deltaYHalf = Math.max(0, deltaY / 2); Rect src = new Rect(deltaXHalf, deltaYHalf, deltaXHalf + Math.min(targetWidth, source.getWidth()), deltaYHalf + Math.min(targetHeight, source.getHeight())); int dstX = (targetWidth - src.width()) / 2; int dstY = (targetHeight - src.height()) / 2; Rect dst = new Rect(dstX, dstY, targetWidth - dstX, targetHeight - dstY); c.drawBitmap(source, src, dst, null); return b2; } float bitmapWidthF = source.getWidth(); float bitmapHeightF = source.getHeight(); float bitmapAspect = bitmapWidthF / bitmapHeightF; float viewAspect = (float) targetWidth / targetHeight; if (bitmapAspect > viewAspect) { float scale = targetHeight / bitmapHeightF; if (scale < .9F || scale > 1F) { scaler.setScale(scale, scale); } else { scaler = null; } } else { float scale = targetWidth / bitmapWidthF; if (scale < .9F || scale > 1F) { scaler.setScale(scale, scale); } else { scaler = null; } } Bitmap b1; if (scaler != null) { // this is used for minithumb and crop, so we want to mFilter here. b1 = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), scaler, true); } else { b1 = source; } int dx1 = Math.max(0, b1.getWidth() - targetWidth); int dy1 = Math.max(0, b1.getHeight() - targetHeight); Bitmap b2 = Bitmap.createBitmap(b1, dx1 / 2, dy1 / 2, targetWidth, targetHeight); if (b1 != source) { b1.recycle(); } return b2; } // Thong added for rotate private Bitmap rotateImage(Bitmap src, float degree) { // create new matrix Matrix matrix = new Matrix(); // setup rotation degree matrix.postRotate(degree); Bitmap bmp = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true); return bmp; } private int getOrientationInDegree(Activity activity) { int rotation = activity.getWindowManager().getDefaultDisplay() .getRotation(); int degrees = 0; switch (rotation) { case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break; } return degrees; } }