diff options
Diffstat (limited to 'src/com/android/camera/CropImage.java')
-rw-r--r-- | src/com/android/camera/CropImage.java | 802 |
1 files changed, 802 insertions, 0 deletions
diff --git a/src/com/android/camera/CropImage.java b/src/com/android/camera/CropImage.java new file mode 100644 index 0000000..cefaf83 --- /dev/null +++ b/src/com/android/camera/CropImage.java @@ -0,0 +1,802 @@ +/* + * 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.android.camera; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +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.os.Handler; +import android.provider.MediaStore; +import android.util.AttributeSet; +import android.util.Config; +import android.util.Log; +import android.view.Menu; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; +import android.widget.Toast; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; + +public class CropImage extends Activity { + private static final String TAG = "CropImage"; + private ProgressDialog mFaceDetectionDialog = null; + private ProgressDialog mSavingProgressDialog = null; + private ImageManager.IImageList mAllImages; + private Bitmap.CompressFormat mSaveFormat = Bitmap.CompressFormat.JPEG; // only used with mSaveUri + private Uri mSaveUri = null; + private int mAspectX, mAspectY; + private int mOutputX, mOutputY; + private boolean mDoFaceDetection = true; + private boolean mCircleCrop = false; + private boolean mWaitingToPick; + private boolean mScale; + private boolean mSaving; + private boolean mScaleUp = true; + + CropImageView mImageView; + ContentResolver mContentResolver; + + Bitmap mBitmap; + Bitmap mCroppedImage; + HighlightView mCrop; + + ImageManager.IImage mImage; + + public CropImage() { + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return super.onCreateOptionsMenu(menu); + } + + static public class CropImageView extends ImageViewTouchBase { + ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>(); + HighlightView mMotionHighlightView = null; + float mLastX, mLastY; + int mMotionEdge; + + public CropImageView(Context context) { + super(context); + } + + @Override + protected boolean doesScrolling() { + return false; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mBitmapDisplayed != null) { + for (HighlightView hv : mHighlightViews) { + hv.mMatrix.set(getImageMatrix()); + hv.invalidate(); + if (hv.mIsFocused) { + centerBasedOnHighlightView(hv); + } + } + } + } + + public CropImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + protected void zoomTo(float scale, float centerX, float centerY) { + super.zoomTo(scale, centerX, centerY); + for (HighlightView hv : mHighlightViews) { + hv.mMatrix.set(getImageMatrix()); + hv.invalidate(); + } + } + + protected void zoomIn() { + super.zoomIn(); + for (HighlightView hv : mHighlightViews) { + hv.mMatrix.set(getImageMatrix()); + hv.invalidate(); + } + } + + protected void zoomOut() { + super.zoomOut(); + for (HighlightView hv : mHighlightViews) { + hv.mMatrix.set(getImageMatrix()); + hv.invalidate(); + } + } + + + @Override + protected boolean usePerfectFitBitmap() { + return false; + } + + @Override + protected void postTranslate(float deltaX, float deltaY) { + super.postTranslate(deltaX, deltaY); + for (int i = 0; i < mHighlightViews.size(); i++) { + HighlightView hv = mHighlightViews.get(i); + hv.mMatrix.postTranslate(deltaX, deltaY); + hv.invalidate(); + } + } + + private void recomputeFocus(MotionEvent event) { + for (int i = 0; i < mHighlightViews.size(); i++) { + HighlightView hv = mHighlightViews.get(i); + hv.setFocus(false); + hv.invalidate(); + } + + for (int i = 0; i < mHighlightViews.size(); i++) { + HighlightView hv = mHighlightViews.get(i); + int edge = hv.getHit(event.getX(), event.getY()); + if (edge != HighlightView.GROW_NONE) { + if (!hv.hasFocus()) { + hv.setFocus(true); + hv.invalidate(); + } + break; + } + } + invalidate(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + CropImage cropImage = (CropImage)mContext; + if (cropImage.mSaving) + return false; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (cropImage.mWaitingToPick) { + recomputeFocus(event); + } else { + for (int i = 0; i < mHighlightViews.size(); i++) { + HighlightView hv = mHighlightViews.get(i); + int edge = hv.getHit(event.getX(), event.getY()); + if (edge != HighlightView.GROW_NONE) { + mMotionEdge = edge; + mMotionHighlightView = hv; + mLastX = event.getX(); + mLastY = event.getY(); + mMotionHighlightView.setMode(edge == HighlightView.MOVE + ? HighlightView.ModifyMode.Move + : HighlightView.ModifyMode.Grow); + break; + } + } + } + break; + case MotionEvent.ACTION_UP: + if (cropImage.mWaitingToPick) { + for (int i = 0; i < mHighlightViews.size(); i++) { + HighlightView hv = mHighlightViews.get(i); + if (hv.hasFocus()) { + cropImage.mCrop = hv; + for (int j = 0; j < mHighlightViews.size(); j++) { + if (j == i) + continue; + mHighlightViews.get(j).setHidden(true); + } + centerBasedOnHighlightView(hv); + ((CropImage)mContext).mWaitingToPick = false; + return true; + } + } + } else if (mMotionHighlightView != null) { + centerBasedOnHighlightView(mMotionHighlightView); + mMotionHighlightView.setMode(HighlightView.ModifyMode.None); + } + mMotionHighlightView = null; + break; + case MotionEvent.ACTION_MOVE: + if (cropImage.mWaitingToPick) { + recomputeFocus(event); + } else if (mMotionHighlightView != null) { + mMotionHighlightView.handleMotion(mMotionEdge, event.getX()-mLastX, event.getY()-mLastY); + mLastX = event.getX(); + mLastY = event.getY(); + + if (true) { + // This section of code is optional. It has some user + // benefit in that moving the crop rectangle against + // the edge of the screen causes scrolling but it means + // that the crop rectangle is no longer fixed under + // the user's finger. + ensureVisible(mMotionHighlightView); + } + } + break; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + center(true, true, true); + break; + case MotionEvent.ACTION_MOVE: + // if we're not zoomed then there's no point in even allowing + // the user to move the image around. This call to center + // puts it back to the normalized location (with false meaning + // don't animate). + if (getScale() == 1F) + center(true, true, false); + break; + } + + return true; + } + + private void ensureVisible(HighlightView hv) { + Rect r = hv.mDrawRect; + + int panDeltaX1 = Math.max(0, mLeft - r.left); + int panDeltaX2 = Math.min(0, mRight - r.right); + + int panDeltaY1 = Math.max(0, mTop - r.top); + int panDeltaY2 = Math.min(0, mBottom - r.bottom); + + int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2; + int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2; + + if (panDeltaX != 0 || panDeltaY != 0) + panBy(panDeltaX, panDeltaY); + } + + private void centerBasedOnHighlightView(HighlightView hv) { + Rect drawRect = hv.mDrawRect; + + float width = drawRect.width(); + float height = drawRect.height(); + + float thisWidth = getWidth(); + float thisHeight = getHeight(); + + float z1 = thisWidth / width * .6F; + float z2 = thisHeight / height * .6F; + + float zoom = Math.min(z1, z2); + zoom = zoom * this.getScale(); + zoom = Math.max(1F, zoom); + + if ((Math.abs(zoom - getScale()) / zoom) > .1) { + float [] coordinates = new float[] { hv.mCropRect.centerX(), hv.mCropRect.centerY() }; + getImageMatrix().mapPoints(coordinates); + zoomTo(zoom, coordinates[0], coordinates[1], 300F); + } + + ensureVisible(hv); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + for (int i = 0; i < mHighlightViews.size(); i++) { + mHighlightViews.get(i).draw(canvas); + } + } + + public HighlightView get(int i) { + return mHighlightViews.get(i); + } + + public int size() { + return mHighlightViews.size(); + } + + public void add(HighlightView hv) { + mHighlightViews.add(hv); + invalidate(); + } + } + + private void fillCanvas(int width, int height, Canvas c) { + Paint paint = new Paint(); + paint.setColor(0x00000000); // pure alpha + paint.setStyle(android.graphics.Paint.Style.FILL); + paint.setAntiAlias(true); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + c.drawRect(0F, 0F, width, height, paint); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + mContentResolver = getContentResolver(); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.cropimage); + + mImageView = (CropImageView) findViewById(R.id.image); + + MenuHelper.showStorageToast(this); + + try { + android.content.Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + if (Config.LOGV) + Log.v(TAG, "extras are " + extras); + if (extras != null) { + for (String s: extras.keySet()) { + if (Config.LOGV) + Log.v(TAG, "" + s + " >>> " + extras.get(s)); + } + if (extras.getString("circleCrop") != null) { + mCircleCrop = true; + mAspectX = 1; + mAspectY = 1; + } + mSaveUri = (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT); + if (mSaveUri != null) { + String compressFormatString = extras.getString("outputFormat"); + if (compressFormatString != null) + mSaveFormat = Bitmap.CompressFormat.valueOf(compressFormatString); + } + mBitmap = (Bitmap) extras.getParcelable("data"); + mAspectX = extras.getInt("aspectX"); + mAspectY = extras.getInt("aspectY"); + mOutputX = extras.getInt("outputX"); + mOutputY = extras.getInt("outputY"); + mScale = extras.getBoolean("scale", true); + mScaleUp = extras.getBoolean("scaleUpIfNeeded", true); + mDoFaceDetection = extras.containsKey("noFaceDetection") ? !extras.getBoolean("noFaceDetection") : true; + } + + if (mBitmap == null) { + Uri target = intent.getData(); + mAllImages = ImageManager.makeImageList(target, CropImage.this, ImageManager.SORT_ASCENDING); + mImage = mAllImages.getImageForUri(target); + if(mImage != null) { + // don't read in really large bitmaps. max out at 1000. + // TODO when saving the resulting bitmap use the decode/crop/encode + // api so we don't lose any resolution + mBitmap = mImage.thumbBitmap(); + if (Config.LOGV) + Log.v(TAG, "thumbBitmap returned " + mBitmap); + } + } + + if (mBitmap == null) { + finish(); + return; + } + + mHandler.postDelayed(new Runnable() { + public void run() { + if (isFinishing()) { + return; + } + mFaceDetectionDialog = ProgressDialog.show(CropImage.this, + null, + getResources().getString(R.string.runningFaceDetection), + true, false); + mImageView.setImageBitmapResetBase(mBitmap, true, true); + if (mImageView.getScale() == 1F) + mImageView.center(true, true, false); + + new Thread(new Runnable() { + public void run() { + final Bitmap b = mImage != null ? mImage.fullSizeBitmap(500) : mBitmap; + if (Config.LOGV) + Log.v(TAG, "back from mImage.fullSizeBitmap(500) with bitmap of size " + b.getWidth() + " / " + b.getHeight()); + mHandler.post(new Runnable() { + public void run() { + if (b != mBitmap && b != null) { + mBitmap = b; + mImageView.setImageBitmapResetBase(b, true, false); + } + if (mImageView.getScale() == 1F) + mImageView.center(true, true, false); + + new Thread(mRunFaceDetection).start(); + } + }); + } + }).start(); + }}, 100); + } catch (Exception e) { + Log.e(TAG, "Failed to load bitmap", e); + finish(); + } + + findViewById(R.id.discard).setOnClickListener(new android.view.View.OnClickListener() { + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + + findViewById(R.id.save).setOnClickListener(new android.view.View.OnClickListener() { + public void onClick(View v) { + // 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 + mSaving = true; + if (mCroppedImage == null) { + if (mCrop == null) { + if (Config.LOGV) + Log.v(TAG, "no cropped image..."); + return; + } + + Rect r = mCrop.getCropRect(); + + int width = r.width(); + int height = r.height(); + + // if we're circle cropping we'll want alpha which is the third param here + mCroppedImage = Bitmap.createBitmap(width, height, + mCircleCrop ? + Bitmap.Config.ARGB_8888 : + Bitmap.Config.RGB_565); + Canvas c1 = new Canvas(mCroppedImage); + c1.drawBitmap(mBitmap, r, new Rect(0, 0, width, height), 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 (mCroppedImage); + android.graphics.Path p = new android.graphics.Path(); + p.addCircle(width/2F, height/2F, width/2F, android.graphics.Path.Direction.CW); + c.clipPath(p, Region.Op.DIFFERENCE); + + fillCanvas(width, height, c); + } + } + + /* 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 */ + mCroppedImage = ImageLoader.transform(new Matrix(), + mCroppedImage, mOutputX, mOutputY, mScaleUp); + } 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 c1 = new Canvas(b); + + /* Draw the cropped bitmap in the center */ + Rect r = mCrop.getCropRect(); + int left = (mOutputX / 2) - (r.width() / 2); + int top = (mOutputY / 2) - (r.width() / 2); + c1.drawBitmap(mBitmap, r, new Rect(left, top, left + + r.width(), top + r.height()), null); + + /* Set the cropped bitmap as the new bitmap */ + mCroppedImage = b; + } + } + + Bundle myExtras = getIntent().getExtras(); + if (myExtras != null && (myExtras.getParcelable("data") != null || myExtras.getBoolean("return-data"))) { + Bundle extras = new Bundle(); + extras.putParcelable("data", mCroppedImage); + setResult(RESULT_OK, + (new Intent()).setAction("inline-data").putExtras(extras)); + finish(); + } else { + if (!isFinishing()) { + mSavingProgressDialog = ProgressDialog.show(CropImage.this, + null, + getResources().getString(R.string.savingImage), + true, true); + } + Runnable r = new Runnable() { + public void run() { + if (mSaveUri != null) { + OutputStream outputStream = null; + try { + String scheme = mSaveUri.getScheme(); + if (scheme.equals("file")) { + outputStream = new FileOutputStream(mSaveUri.toString().substring(scheme.length()+":/".length())); + } else { + outputStream = mContentResolver.openOutputStream(mSaveUri); + } + if (outputStream != null) + mCroppedImage.compress(mSaveFormat, 75, outputStream); + + } catch (IOException ex) { + if (Config.LOGV) + Log.v(TAG, "got IOException " + ex); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException ex) { + + } + } + } + Bundle extras = new Bundle(); + setResult(RESULT_OK, + (new Intent()) + .setAction(mSaveUri.toString()) + .putExtras(extras)); + } else { + Bundle extras = new Bundle(); + extras.putString("rect", mCrop.getCropRect().toString()); + + // here we decide whether to create a new image or + // modify the existing image + if (false) { + /* + // this is the "modify" case + ImageManager.IGetBoolean_cancelable cancelable = + mImage.saveImageContents(mCroppedImage, null, null, null, mImage.getDateTaken(), 0, false); + boolean didSave = cancelable.get(); + extras.putString("thumb1uri", mImage.thumbUri().toString()); + setResult(RESULT_OK, + (new Intent()).setAction(mImage.fullSizeImageUri().toString()) + .putExtras(extras)); + */ + } else { + // this is the "new image" case + java.io.File oldPath = new java.io.File(mImage.getDataPath()); + java.io.File directory = new java.io.File(oldPath.getParent()); + + int x = 0; + String fileName = oldPath.getName(); + fileName = fileName.substring(0, fileName.lastIndexOf(".")); + + while (true) { + x += 1; + String candidate = directory.toString() + "/" + fileName + "-" + x + ".jpg"; + if (Config.LOGV) + Log.v(TAG, "candidate is " + candidate); + boolean exists = (new java.io.File(candidate)).exists(); + if (!exists) + break; + } + + try { + Uri newUri = ImageManager.instance().addImage( + CropImage.this, + getContentResolver(), + mImage.getTitle(), + mImage.getDescription(), + mImage.getDateTaken(), + null, // TODO this null is going to cause us to lose the location (gps) + 0, // TODO this is going to cause the orientation to reset + directory.toString(), + fileName + "-" + x + ".jpg"); + + ImageManager.IAddImage_cancelable cancelable = ImageManager.instance().storeImage( + newUri, + CropImage.this, + getContentResolver(), + 0, // TODO fix this orientation + mCroppedImage, + null); + + cancelable.get(); + setResult(RESULT_OK, + (new Intent()).setAction(newUri.toString()) + .putExtras(extras)); + } catch (Exception ex) { + // basically ignore this or put up + // some ui saying we failed + } + } + } + finish(); + } + }; + Thread t = new Thread(r); + t.start(); + } + } + }); + } + + @Override + public void onResume() { + super.onResume(); + } + + Handler mHandler = new Handler(); + + Runnable mRunFaceDetection = new Runnable() { + float mScale = 1F; + RectF mUnion = null; + Matrix mImageMatrix; + FaceDetector.Face[] mFaces = new FaceDetector.Face[3]; + int mNumFaces; + + 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 = makeHighlightView(); + + 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); + + if (mUnion == null) { + mUnion = new RectF(faceRect); + } else { + mUnion.union(faceRect); + } + + mImageView.add(hv); + } + + private HighlightView makeHighlightView() { + return new HighlightView(mImageView); + } + + private void makeDefault() { + HighlightView hv = makeHighlightView(); + + 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; + + if (mAspectX != 0 && mAspectY != 0) { + if (mAspectX > mAspectY) { + cropHeight = cropWidth * mAspectY / mAspectX; +// Log.v(TAG, "adjusted cropHeight to " + cropHeight); + } else { + cropWidth = cropHeight * mAspectX / mAspectY; +// Log.v(TAG, "adjusted cropWidth to " + cropWidth); + } + } + + 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.add(hv); + } + + private Bitmap prepareBitmap() { + if (mBitmap == null) + return null; + + // scale the image down for faster face detection + // 256 pixels wide is enough. + if (mBitmap.getWidth() > 256) { + mScale = 256.0F / (float) mBitmap.getWidth(); + } + Matrix matrix = new Matrix(); + matrix.setScale(mScale, mScale); + Bitmap faceBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap + .getWidth(), mBitmap.getHeight(), matrix, true); + return faceBitmap; + } + + 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 (Config.LOGV) + Log.v(TAG, "numFaces is " + mNumFaces); + } + 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); + } + + closeProgressDialog(); + + if (mNumFaces > 1) { + Toast t = Toast.makeText(CropImage.this, R.string.multiface_crop_help, Toast.LENGTH_SHORT); + t.show(); + } + } + }); + + } + }; + + @Override + public void onStop() { + closeProgressDialog(); + super.onStop(); + if (mAllImages != null) + mAllImages.deactivate(); + } + + private synchronized void closeProgressDialog() { + if (mFaceDetectionDialog != null) { + mFaceDetectionDialog.dismiss(); + mFaceDetectionDialog = null; + } + if (mSavingProgressDialog != null) { + mSavingProgressDialog.dismiss(); + mSavingProgressDialog = null; + } + } +} |