diff options
6 files changed, 648 insertions, 251 deletions
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java index 3e1835853..a6ed0d3cb 100644 --- a/src/com/android/gallery3d/app/PhotoPage.java +++ b/src/com/android/gallery3d/app/PhotoPage.java @@ -60,6 +60,7 @@ import com.android.gallery3d.data.SnailAlbum; import com.android.gallery3d.data.SnailItem; import com.android.gallery3d.data.SnailSource; import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.crop.CropActivity; import com.android.gallery3d.picasasource.PicasaSource; import com.android.gallery3d.ui.DetailsHelper; import com.android.gallery3d.ui.DetailsHelper.CloseListener; @@ -1086,8 +1087,8 @@ public abstract class PhotoPage extends ActivityState implements } case R.id.action_crop: { Activity activity = mActivity; - Intent intent = new Intent(FilterShowActivity.CROP_ACTION); - intent.setClass(activity, FilterShowActivity.class); + Intent intent = new Intent(CropActivity.CROP_ACTION); + intent.setClass(activity, CropActivity.class); intent.setDataAndType(manager.getContentUri(path), current.getMimeType()) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current) diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java index 26659a600..878b82fc6 100644 --- a/src/com/android/gallery3d/filtershow/crop/CropActivity.java +++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java @@ -21,11 +21,10 @@ import android.app.Activity; import android.app.WallpaperManager; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; -import android.graphics.Point; +import android.graphics.BitmapRegionDecoder; import android.graphics.Rect; import android.graphics.RectF; import android.net.Uri; @@ -34,8 +33,6 @@ import android.os.Bundle; import android.provider.MediaStore; import android.util.DisplayMetrics; import android.util.Log; -import android.util.TypedValue; -import android.view.Display; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager; @@ -43,8 +40,11 @@ import android.widget.Toast; import com.android.gallery3d.R; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; /** @@ -52,19 +52,30 @@ import java.io.OutputStream; */ public class CropActivity extends Activity { private static final String LOGTAG = "CropActivity"; + public static final String CROP_ACTION = "com.android.camera.action.CROP"; private CropExtras mCropExtras = null; private LoadBitmapTask mLoadBitmapTask = null; - private SaveBitmapTask mSaveBitmapTask = null; - private SetWallpaperTask mSetWallpaperTask = null; + private Bitmap mOriginalBitmap = null; + private RectF mOriginalBounds = null; + private Uri mSourceUri = null; private CropView mCropView = null; - private int mActiveBackgroundIO = 0; + private View mSaveButton = null; + private boolean finalIOGuard = false; + private Intent mResultIntent = null; private static final int SELECT_PICTURE = 1; // request code for picker - private static final int DEFAULT_DENSITY = 133; + private static final int DEFAULT_COMPRESS_QUALITY = 90; public static final int MAX_BMAP_IN_INTENT = 990000; + // Flags + private static final int DO_SET_WALLPAPER = 1; + private static final int DO_RETURN_DATA = 1 << 1; + private static final int DO_EXTRA_OUTPUT = 1 << 2; + + private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -79,22 +90,30 @@ public class CropActivity extends Activity { setContentView(R.layout.crop_activity); mCropView = (CropView) findViewById(R.id.cropView); - if (intent.getData() != null) { - startLoadBitmap(intent.getData()); - } else { - pickImage(); - } ActionBar actionBar = getActionBar(); actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); actionBar.setCustomView(R.layout.filtershow_actionbar); - View saveButton = actionBar.getCustomView(); - saveButton.setOnClickListener(new OnClickListener() { + View mSaveButton = actionBar.getCustomView(); + mSaveButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { startFinishOutput(); } }); + + if (intent.getData() != null) { + mSourceUri = intent.getData(); + startLoadBitmap(mSourceUri); + } else { + pickImage(); + } + } + + private void enableSave(boolean enable) { + if (mSaveButton != null) { + mSaveButton.setEnabled(enable); + } } @Override @@ -109,7 +128,7 @@ public class CropActivity extends Activity { * Opens a selector in Gallery to chose an image for use when none was given * in the CROP intent. */ - public void pickImage() { + private void pickImage() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); @@ -123,53 +142,25 @@ public class CropActivity extends Activity { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) { - Uri selectedImageUri = data.getData(); - startLoadBitmap(selectedImageUri); + mSourceUri = data.getData(); + startLoadBitmap(mSourceUri); } } /** - * Gets the crop extras from the intent, or null if none exist. - */ - public static CropExtras getExtrasFromIntent(Intent intent) { - Bundle extras = intent.getExtras(); - if (extras != null) { - return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0), - extras.getInt(CropExtras.KEY_OUTPUT_Y, 0), - extras.getBoolean(CropExtras.KEY_SCALE, true) && - extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false), - extras.getInt(CropExtras.KEY_ASPECT_X, 0), - extras.getInt(CropExtras.KEY_ASPECT_Y, 0), - extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false), - extras.getBoolean(CropExtras.KEY_RETURN_DATA, false), - (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT), - extras.getString(CropExtras.KEY_OUTPUT_FORMAT), - extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false), - extras.getFloat(CropExtras.KEY_SPOTLIGHT_X), - extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y)); - } - return null; - } - - /** * Gets screen size metric. */ private int getScreenImageSize() { - DisplayMetrics metrics = new DisplayMetrics(); - Display display = getWindowManager().getDefaultDisplay(); - Point size = new Point(); - display.getSize(size); - display.getMetrics(metrics); - int msize = Math.min(size.x, size.y); - // TODO: WTF - return (DEFAULT_DENSITY * msize) / metrics.densityDpi + 512; + DisplayMetrics outMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(outMetrics); + return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels); } /** * Method that loads a bitmap in an async task. */ private void startLoadBitmap(Uri uri) { - mActiveBackgroundIO++; + enableSave(false); final View loading = findViewById(R.id.loading); loading.setVisibility(View.VISIBLE); mLoadBitmapTask = new LoadBitmapTask(); @@ -179,18 +170,19 @@ public class CropActivity extends Activity { /** * Method called on UI thread with loaded bitmap. */ - private void doneLoadBitmap(Bitmap bitmap) { - mActiveBackgroundIO--; + private void doneLoadBitmap(Bitmap bitmap, RectF bounds) { final View loading = findViewById(R.id.loading); loading.setVisibility(View.GONE); mOriginalBitmap = bitmap; - // TODO: move these to dimens folder - if (bitmap != null) { - mCropView.setup(bitmap, (int) getPixelsFromDip(55), (int) getPixelsFromDip(25)); + mOriginalBounds = bounds; + if (bitmap != null && bitmap.getWidth() != 0 && bitmap.getHeight() != 0) { + RectF imgBounds = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); + mCropView.initialize(bitmap, imgBounds, imgBounds, 0); + enableSave(true); } else { Log.w(LOGTAG, "could not load image for cropping"); cannotLoadImage(); - setResult(RESULT_CANCELED, mResultIntent); + setResult(RESULT_CANCELED, new Intent()); done(); } } @@ -217,7 +209,6 @@ public class CropActivity extends Activity { public LoadBitmapTask() { mBitmapSize = getScreenImageSize(); - Log.v(LOGTAG, "bitmap size: " + mBitmapSize); mContext = getApplicationContext(); mOriginalBounds = new Rect(); } @@ -231,155 +222,279 @@ public class CropActivity extends Activity { @Override protected void onPostExecute(Bitmap result) { - doneLoadBitmap(result); - // super.onPostExecute(result); + doneLoadBitmap(result, new RectF(mOriginalBounds)); } } - private void startSaveBitmap(Bitmap bmap, Uri uri, String format) { - if (bmap == null || uri == null) { - throw new IllegalArgumentException("bad argument to startSaveBitmap"); + private void startFinishOutput() { + if (finalIOGuard) { + return; + } else { + finalIOGuard = true; } - mActiveBackgroundIO++; + enableSave(false); + Uri destinationUri = null; + int flags = 0; + if (mOriginalBitmap != null && mCropExtras != null) { + if (mCropExtras.getExtraOutput() != null) { + destinationUri = mCropExtras.getExtraOutput(); + flags |= DO_EXTRA_OUTPUT; + } + if (mCropExtras.getSetAsWallpaper()) { + flags |= DO_SET_WALLPAPER; + } + if (mCropExtras.getReturnData()) { + flags |= DO_RETURN_DATA; + } + } + if (flags == 0) { + destinationUri = CropLoader.makeAndInsertUri(this, mSourceUri); + if (destinationUri != null) { + flags |= DO_EXTRA_OUTPUT; + } + } + if ((flags & FLAG_CHECK) != 0) { + RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight()); + RectF crop = getBitmapCrop(photo); + startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop, + photo, mOriginalBounds, + (mCropExtras == null) ? null : mCropExtras.getOutputFormat()); + return; + } + setResult(RESULT_CANCELED, mResultIntent); + done(); + return; + } + + private void startBitmapIO(int flags, Bitmap currentBitmap, Uri sourceUri, Uri destUri, + RectF cropBounds, RectF photoBounds, RectF currentBitmapBounds, String format) { + if (cropBounds == null || photoBounds == null || currentBitmap == null + || currentBitmap.getWidth() == 0 || currentBitmap.getHeight() == 0 + || cropBounds.width() == 0 || cropBounds.height() == 0 || photoBounds.width() == 0 + || photoBounds.height() == 0) { + return; // fail fast + } + if ((flags & FLAG_CHECK) == 0) { + return; // no output options + } + if ((flags & DO_SET_WALLPAPER) != 0) { + Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show(); + } + final View loading = findViewById(R.id.loading); loading.setVisibility(View.VISIBLE); - mSaveBitmapTask = new SaveBitmapTask(uri, format); - mSaveBitmapTask.execute(bmap); + BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds, + photoBounds, currentBitmapBounds); + ioTask.execute(currentBitmap); } - private void doneSaveBitmap(Uri uri) { - mActiveBackgroundIO--; + private void doneBitmapIO(boolean success, Intent intent) { final View loading = findViewById(R.id.loading); loading.setVisibility(View.GONE); - if (uri == null) { - Log.w(LOGTAG, "failed to save bitmap"); - setResult(RESULT_CANCELED, mResultIntent); - done(); - return; + if (success) { + setResult(RESULT_OK, intent); + } else { + setResult(RESULT_CANCELED, intent); } done(); } - private class SaveBitmapTask extends AsyncTask<Bitmap, Void, Boolean> { + private class BitmapIOTask extends AsyncTask<Bitmap, Void, Boolean> { + private final WallpaperManager mWPManager; + InputStream mInStream = null; OutputStream mOutStream = null; String mOutputFormat = null; Uri mOutUri = null; - - public SaveBitmapTask(Uri uri, String outputFormat) { + Uri mInUri = null; + int mFlags = 0; + RectF mCrop = null; + RectF mPhoto = null; + RectF mOrig = null; + Intent mResultIntent = null; + + public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags, + RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds) { mOutputFormat = outputFormat; mOutStream = null; - mOutUri = uri; - try { - mOutStream = getContentResolver().openOutputStream(uri); - } catch (FileNotFoundException e) { - Log.w(LOGTAG, "cannot write output: " + mOutUri.toString(), e); - } - } + mOutUri = destUri; + mInUri = sourceUri; + mFlags = flags; + mCrop = cropBounds; + mPhoto = photoBounds; + mOrig = originalBitmapBounds; + mWPManager = WallpaperManager.getInstance(getApplicationContext()); + mResultIntent = new Intent(); - @Override - protected Boolean doInBackground(Bitmap... params) { - if (mOutStream == null) { - return false; + if ((flags & DO_EXTRA_OUTPUT) != 0) { + if (mOutUri == null) { + Log.w(LOGTAG, "cannot write file, no output URI given"); + } else { + try { + mOutStream = getContentResolver().openOutputStream(mOutUri); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "cannot write file: " + mOutUri.toString(), e); + } + } } - CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); - return params[0].compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream); - } - @Override - protected void onPostExecute(Boolean result) { - if (result.booleanValue() == false) { - Log.w(LOGTAG, "could not compress to output: " + mOutUri.toString()); - doneSaveBitmap(null); + if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) { + if (mInUri == null) { + Log.w(LOGTAG, "cannot read original file, no input URI given"); + } else { + try { + mInStream = getContentResolver().openInputStream(mInUri); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); + } + } } - doneSaveBitmap(mOutUri); - } - } - - private void startSetWallpaper(Bitmap bmap) { - if (bmap == null) { - throw new IllegalArgumentException("bad argument to startSetWallpaper"); - } - mActiveBackgroundIO++; - Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show(); - mSetWallpaperTask = new SetWallpaperTask(); - mSetWallpaperTask.execute(bmap); - - } - - private void doneSetWallpaper() { - mActiveBackgroundIO--; - done(); - } - - private class SetWallpaperTask extends AsyncTask<Bitmap, Void, Boolean> { - private final WallpaperManager mWPManager; - - public SetWallpaperTask() { - mWPManager = WallpaperManager.getInstance(getApplicationContext()); } @Override protected Boolean doInBackground(Bitmap... params) { - try { - mWPManager.setBitmap(params[0]); - } catch (IOException e) { - Log.w(LOGTAG, "fail to set wall paper", e); + boolean failure = false; + Bitmap img = params[0]; + + // Set extra for crop bounds + if (mCrop != null && mPhoto != null && mOrig != null) { + RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig); + if (trueCrop != null) { + Rect rounded = new Rect(); + trueCrop.roundOut(rounded); + mResultIntent.putExtra(CropExtras.KEY_CROPPED_RECT, rounded); + } } - return true; - } - @Override - protected void onPostExecute(Boolean result) { - doneSetWallpaper(); - } - } - - private void startFinishOutput() { - if (mOriginalBitmap != null && mCropExtras != null) { - Bitmap cropped = null; - if (mCropExtras.getExtraOutput() != null) { - if (cropped == null) { - cropped = getCroppedImage(mOriginalBitmap); + // Find the small cropped bitmap that is returned in the intent + if ((mFlags & DO_RETURN_DATA) != 0) { + assert (img != null); + Bitmap ret = getCroppedImage(img, mCrop, mPhoto); + if (ret != null) { + ret = getDownsampledBitmap(ret, MAX_BMAP_IN_INTENT); } - startSaveBitmap(cropped, mCropExtras.getExtraOutput(), - mCropExtras.getOutputFormat()); - } - if (mCropExtras.getSetAsWallpaper()) { - if (cropped == null) { - cropped = getCroppedImage(mOriginalBitmap); + if (ret == null) { + Log.w(LOGTAG, "could not downsample bitmap to return in data"); + failure = true; + } else { + mResultIntent.putExtra(CropExtras.KEY_DATA, ret); } - startSetWallpaper(cropped); } - if (mCropExtras.getReturnData()) { - if (cropped == null) { - cropped = getCroppedImage(mOriginalBitmap); + + // Do the large cropped bitmap and/or set the wallpaper + if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) { + BitmapRegionDecoder decoder = null; + try { + decoder = BitmapRegionDecoder.newInstance(mInStream, true); + } catch (IOException e) { + Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); + } + if (decoder == null) { + failure = true; + return false; + } + + // Find crop bounds (scaled to original image size) + RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig); + if (trueCrop == null) { + Log.w(LOGTAG, "cannot find crop for full size image"); + failure = true; + return false; + } + Rect roundedTrueCrop = new Rect(); + trueCrop.roundOut(roundedTrueCrop); + + if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { + Log.w(LOGTAG, "crop has bad values for full size image"); + failure = true; + return false; + } + // Do region decoding to get crop bitmap + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + Bitmap crop = decoder.decodeRegion(roundedTrueCrop, options); + decoder.recycle(); + + if (crop == null) { + Log.w(LOGTAG, "cannot region decode file: " + mInUri.toString()); + failure = true; + return false; } - int bmapSize = cropped.getRowBytes() * cropped.getHeight(); - if (bmapSize > MAX_BMAP_IN_INTENT) { - Log.w(LOGTAG, "Bitmap too large to be returned via intent"); + // Get output compression format + CompressFormat cf = + convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); + + // If we only need to output to a URI, compress straight to file + if (mFlags == DO_EXTRA_OUTPUT) { + if (mOutStream == null + || !crop.compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream)) { + Log.w(LOGTAG, "failed to compress bitmap to file: " + mOutUri.toString()); + failure = true; + } } else { - mResultIntent.putExtra(CropExtras.KEY_DATA, cropped); + // Compress to byte array + ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); + if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) { + + // If we need to output to a Uri, write compressed + // bitmap out + if ((mFlags & DO_EXTRA_OUTPUT) != 0) { + if (mOutStream == null) { + Log.w(LOGTAG, + "failed to compress bitmap to file: " + mOutUri.toString()); + failure = true; + } else { + try { + mOutStream.write(tmpOut.toByteArray()); + } catch (IOException e) { + Log.w(LOGTAG, + "failed to compress bitmap to file: " + + mOutUri.toString(), e); + failure = true; + } + } + } + + // If we need to set to the wallpaper, set it + if ((mFlags & DO_SET_WALLPAPER) != 0 && mWPManager != null) { + if (mWPManager == null) { + Log.w(LOGTAG, "no wallpaper manager"); + failure = true; + } else { + try { + mWPManager.setStream(new ByteArrayInputStream(tmpOut + .toByteArray())); + } catch (IOException e) { + Log.w(LOGTAG, "cannot write stream to wallpaper", e); + failure = true; + } + } + } + } else { + Log.w(LOGTAG, "cannot compress bitmap"); + failure = true; + } } } - setResult(RESULT_OK, mResultIntent); - } else { - setResult(RESULT_CANCELED, mResultIntent); + return !failure; // True if any of the operations failed } - done(); + + @Override + protected void onPostExecute(Boolean result) { + doneBitmapIO(result.booleanValue(), mResultIntent); + } + } private void done() { - if (mActiveBackgroundIO == 0) { - finish(); - } + finish(); } - private Bitmap getCroppedImage(Bitmap image) { + protected static Bitmap getCroppedImage(Bitmap image, RectF cropBounds, RectF photoBounds) { RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight()); - RectF crop = getBitmapCrop(imageBounds); + RectF crop = CropMath.getScaledCropBounds(cropBounds, photoBounds, imageBounds); if (crop == null) { - return image; + return null; } Rect intCrop = new Rect(); crop.roundOut(intCrop); @@ -387,29 +502,56 @@ public class CropActivity extends Activity { intCrop.height()); } - private RectF getBitmapCrop(RectF imageBounds) { - RectF crop = new RectF(); - if (!mCropView.getCropBounds(crop, imageBounds)) { - Log.w(LOGTAG, "could not get crop"); + protected static Bitmap getDownsampledBitmap(Bitmap image, int max_size) { + if (image == null || image.getWidth() == 0 || image.getHeight() == 0 || max_size < 16) { + throw new IllegalArgumentException("Bad argument to getDownsampledBitmap()"); + } + int shifts = 0; + int size = CropMath.getBitmapSize(image); + while (size > max_size) { + shifts++; + size /= 4; + } + Bitmap ret = Bitmap.createScaledBitmap(image, image.getWidth() >> shifts, + image.getHeight() >> shifts, true); + if (ret == null) { return null; } - return crop; + // Handle edge case for rounding. + if (CropMath.getBitmapSize(ret) > max_size) { + return Bitmap.createScaledBitmap(ret, ret.getWidth() >> 1, ret.getHeight() >> 1, true); + } + return ret; } /** - * Helper method for unit conversions. + * Gets the crop extras from the intent, or null if none exist. */ - public float getPixelsFromDip(float value) { - Resources r = getResources(); - return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, - r.getDisplayMetrics()); + protected static CropExtras getExtrasFromIntent(Intent intent) { + Bundle extras = intent.getExtras(); + if (extras != null) { + return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0), + extras.getInt(CropExtras.KEY_OUTPUT_Y, 0), + extras.getBoolean(CropExtras.KEY_SCALE, true) && + extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false), + extras.getInt(CropExtras.KEY_ASPECT_X, 0), + extras.getInt(CropExtras.KEY_ASPECT_Y, 0), + extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false), + extras.getBoolean(CropExtras.KEY_RETURN_DATA, false), + (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT), + extras.getString(CropExtras.KEY_OUTPUT_FORMAT), + extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false), + extras.getFloat(CropExtras.KEY_SPOTLIGHT_X), + extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y)); + } + return null; } - private static CompressFormat convertExtensionToCompressFormat(String extension) { + protected static CompressFormat convertExtensionToCompressFormat(String extension) { return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; } - private static String getFileExtension(String requestFormat) { + protected static String getFileExtension(String requestFormat) { String outputFormat = (requestFormat == null) ? "jpg" : requestFormat; @@ -419,4 +561,14 @@ public class CropActivity extends Activity { : "jpg"; } + private RectF getBitmapCrop(RectF imageBounds) { + RectF crop = mCropView.getCrop(); + RectF photo = mCropView.getPhoto(); + if (crop == null || photo == null) { + Log.w(LOGTAG, "could not get crop"); + return null; + } + RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds); + return scaledCrop; + } } diff --git a/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java index 483cb6372..a3664d764 100644 --- a/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java +++ b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java @@ -113,4 +113,17 @@ public abstract class CropDrawingUtils { return m.setRectToRect(imageBounds, displayBounds, Matrix.ScaleToFit.CENTER); } + public static boolean setImageToScreenMatrix(Matrix dst, RectF image, + RectF screen, int rotation) { + RectF rotatedImage = new RectF(); + dst.setRotate(rotation, image.centerX(), image.centerY()); + if (!dst.mapRect(rotatedImage, image)) { + return false; // fails for rotations that are not multiples of 90 + // degrees + } + boolean rToR = dst.setRectToRect(rotatedImage, screen, Matrix.ScaleToFit.CENTER); + boolean rot = dst.preRotate(rotation, image.centerX(), image.centerY()); + return rToR && rot; + } + } diff --git a/src/com/android/gallery3d/filtershow/crop/CropLoader.java b/src/com/android/gallery3d/filtershow/crop/CropLoader.java index 132d6c1dc..2eb1a14ac 100644 --- a/src/com/android/gallery3d/filtershow/crop/CropLoader.java +++ b/src/com/android/gallery3d/filtershow/crop/CropLoader.java @@ -17,6 +17,7 @@ package com.android.gallery3d.filtershow.crop; import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; @@ -24,19 +25,25 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Rect; import android.net.Uri; +import android.os.Environment; import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; import android.util.Log; import com.android.gallery3d.common.Utils; import com.android.gallery3d.exif.ExifInterface; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.sql.Date; +import java.text.SimpleDateFormat; /** * This class contains static methods for loading a bitmap and - * mantains no instance state. + * maintains no instance state. */ public abstract class CropLoader { public static final String LOGTAG = "CropLoader"; @@ -46,6 +53,8 @@ public abstract class CropLoader { public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT; public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM; + private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss"; + public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; /** * Returns the orientation of image at the given URI as one of 0, 90, 180, * 270. @@ -199,4 +208,112 @@ public abstract class CropLoader { return null; } + // TODO: Super gnarly (copied from SaveCopyTask.java), do cleanup. + + public static File getFinalSaveDirectory(Context context, Uri sourceUri) { + File saveDirectory = getSaveDirectory(context, sourceUri); + if ((saveDirectory == null) || !saveDirectory.canWrite()) { + saveDirectory = new File(Environment.getExternalStorageDirectory(), + DEFAULT_SAVE_DIRECTORY); + } + // Create the directory if it doesn't exist + if (!saveDirectory.exists()) + saveDirectory.mkdirs(); + return saveDirectory; + } + + + + public static String getNewFileName(long time) { + return new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time)); + } + + public static File getNewFile(Context context, Uri sourceUri, String filename) { + File saveDirectory = getFinalSaveDirectory(context, sourceUri); + return new File(saveDirectory, filename + ".JPG"); + } + + private interface ContentResolverQueryCallback { + + void onCursorResult(Cursor cursor); + } + + private static void querySource(Context context, Uri sourceUri, String[] projection, + ContentResolverQueryCallback callback) { + ContentResolver contentResolver = context.getContentResolver(); + Cursor cursor = null; + try { + cursor = contentResolver.query(sourceUri, projection, null, null, + null); + if ((cursor != null) && cursor.moveToNext()) { + callback.onCursorResult(cursor); + } + } catch (Exception e) { + // Ignore error for lacking the data column from the source. + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private static File getSaveDirectory(Context context, Uri sourceUri) { + final File[] dir = new File[1]; + querySource(context, sourceUri, new String[] { + ImageColumns.DATA }, new ContentResolverQueryCallback() { + @Override + public void onCursorResult(Cursor cursor) { + dir[0] = new File(cursor.getString(0)).getParentFile(); + } + }); + return dir[0]; + } + + public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName, + long time) { + time /= 1000; + + final ContentValues values = new ContentValues(); + values.put(Images.Media.TITLE, saveFileName); + values.put(Images.Media.DISPLAY_NAME, file.getName()); + values.put(Images.Media.MIME_TYPE, "image/jpeg"); + values.put(Images.Media.DATE_TAKEN, time); + values.put(Images.Media.DATE_MODIFIED, time); + values.put(Images.Media.DATE_ADDED, time); + values.put(Images.Media.ORIENTATION, 0); + values.put(Images.Media.DATA, file.getAbsolutePath()); + values.put(Images.Media.SIZE, file.length()); + + final String[] projection = new String[] { + ImageColumns.DATE_TAKEN, + ImageColumns.LATITUDE, ImageColumns.LONGITUDE, + }; + querySource(context, sourceUri, projection, + new ContentResolverQueryCallback() { + + @Override + public void onCursorResult(Cursor cursor) { + values.put(Images.Media.DATE_TAKEN, cursor.getLong(0)); + + double latitude = cursor.getDouble(1); + double longitude = cursor.getDouble(2); + // TODO: Change || to && after the default location + // issue is fixed. + if ((latitude != 0f) || (longitude != 0f)) { + values.put(Images.Media.LATITUDE, latitude); + values.put(Images.Media.LONGITUDE, longitude); + } + } + }); + + return context.getContentResolver().insert( + Images.Media.EXTERNAL_CONTENT_URI, values); + } + + public static Uri makeAndInsertUri(Context context, Uri sourceUri) { + long time = System.currentTimeMillis(); + String filename = getNewFileName(time); + File file = getNewFile(context, sourceUri, filename); + return insertContent(context, sourceUri, file, filename, time); + } } diff --git a/src/com/android/gallery3d/filtershow/crop/CropMath.java b/src/com/android/gallery3d/filtershow/crop/CropMath.java index 52b11a56b..ed800c912 100644 --- a/src/com/android/gallery3d/filtershow/crop/CropMath.java +++ b/src/com/android/gallery3d/filtershow/crop/CropMath.java @@ -16,6 +16,7 @@ package com.android.gallery3d.filtershow.crop; +import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.RectF; @@ -205,6 +206,35 @@ public class CropMath { r.set(centX - hw, centY - hh, centX + hw, centY + hh); } + /** + * Stretches/Scales/Translates photoBounds to match displayBounds, and + * and returns an equivalent stretched/scaled/translated cropBounds or null + * if the mapping is invalid. + * @param cropBounds cropBounds to transform + * @param photoBounds original bounds containing crop bounds + * @param displayBounds final bounds for crop + * @return the stretched/scaled/translated crop bounds that fit within displayBounds + */ + public static RectF getScaledCropBounds(RectF cropBounds, RectF photoBounds, + RectF displayBounds) { + Matrix m = new Matrix(); + m.setRectToRect(photoBounds, displayBounds, Matrix.ScaleToFit.FILL); + RectF trueCrop = new RectF(cropBounds); + if (!m.mapRect(trueCrop)) { + return null; + } + return trueCrop; + } + + /** + * Returns the size of a bitmap in bytes. + * @param bmap bitmap whose size to check + * @return bitmap size in bytes + */ + public static int getBitmapSize(Bitmap bmap) { + return bmap.getRowBytes() * bmap.getHeight(); + } + private static float getUnrotated(float[] rotatedRect, float[] center, RectF unrotated) { float dy = rotatedRect[1] - rotatedRect[3]; float dx = rotatedRect[0] - rotatedRect[2]; diff --git a/src/com/android/gallery3d/filtershow/crop/CropView.java b/src/com/android/gallery3d/filtershow/crop/CropView.java index d762ad459..56579f4dc 100644 --- a/src/com/android/gallery3d/filtershow/crop/CropView.java +++ b/src/com/android/gallery3d/filtershow/crop/CropView.java @@ -25,30 +25,46 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; +import android.graphics.drawable.NinePatchDrawable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.crop.CropDrawingUtils; + public class CropView extends View { private static final String LOGTAG = "CropView"; - Bitmap mImage = null; - CropObject mCropObj = null; + private RectF mImageBounds = new RectF(); + private RectF mScreenBounds = new RectF(); + private RectF mScreenImageBounds = new RectF(); + private RectF mScreenCropBounds = new RectF(); + private Rect mShadowBounds = new Rect(); + + private Bitmap mBitmap; + private Paint mPaint = new Paint(); + + private NinePatchDrawable mShadow; + private CropObject mCropObj = null; private final Drawable mCropIndicator; private final int mIndicatorSize; + private int mRotation = 0; + private boolean mMovingBlock = false; + private Matrix mDisplayMatrix = null; + private Matrix mDisplayMatrixInverse = null; + private boolean mDirty = false; private float mPrevX = 0; private float mPrevY = 0; - private int mMinSideSize = 45; - private int mTouchTolerance = 20; - private boolean mMovingBlock = false; - - private Matrix mDisplayMatrix = null; - private Matrix mDisplayMatrixInverse = null; + private int mShadowMargin = 15; + private int mMargin = 32; + private int mOverlayShadowColor = 0xCF000000; + private int mMinSideSize = 90; + private int mTouchTolerance = 40; private enum Mode { NONE, MOVE @@ -58,57 +74,41 @@ public class CropView extends View { public CropView(Context context, AttributeSet attrs) { super(context, attrs); - Resources resources = context.getResources(); - mCropIndicator = resources.getDrawable(R.drawable.camera_crop); - mIndicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size); + Resources rsc = context.getResources(); + mShadow = (NinePatchDrawable) rsc.getDrawable(R.drawable.geometry_shadow); + mCropIndicator = rsc.getDrawable(R.drawable.camera_crop); + mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size); + mShadowMargin = (int) rsc.getDimension(R.dimen.shadow_margin); + mMargin = (int) rsc.getDimension(R.dimen.preview_margin); + mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side); + mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance); + mOverlayShadowColor = (int) rsc.getColor(R.color.crop_shadow_color); } - // For unchanging parameters - public void setup(Bitmap image, int minSideSize, int touchTolerance) { - mImage = image; - mMinSideSize = minSideSize; - mTouchTolerance = touchTolerance; - reset(); - } - - @Override - public void onDraw(Canvas canvas) { - if (mImage == null) { - return; - } - int displayWidth = getWidth(); - int displayHeight = getHeight(); - Rect imageBoundsOriginal = new Rect(0, 0, mImage.getWidth(), mImage.getHeight()); - Rect displayBoundsOriginal = new Rect(0, 0, displayWidth, displayHeight); - if (mCropObj == null) { - reset(); - mCropObj = new CropObject(imageBoundsOriginal, imageBoundsOriginal, 0); - } - - RectF imageBounds = mCropObj.getInnerBounds(); - RectF displayBounds = mCropObj.getOuterBounds(); - - // If display matrix doesn't exist, create it and its dependencies - if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { - mDisplayMatrix = CropDrawingUtils.getBitmapToDisplayMatrix(displayBounds, new RectF( - displayBoundsOriginal)); - mDisplayMatrixInverse = new Matrix(); - mDisplayMatrixInverse.reset(); - if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) { - Log.w(LOGTAG, "could not invert display matrix"); + public void initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation) { + mBitmap = image; + if (mCropObj != null) { + RectF crop = mCropObj.getInnerBounds(); + RectF containing = mCropObj.getOuterBounds(); + if (crop != newCropBounds || containing != newPhotoBounds + || mRotation != rotation) { + mRotation = rotation; + mCropObj.resetBoundsTo(newCropBounds, newPhotoBounds); + clearDisplay(); } - // Scale min side and tolerance by display matrix scale factor - mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize)); - mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance)); + } else { + mRotation = rotation; + mCropObj = new CropObject(newPhotoBounds, newCropBounds, 0); + clearDisplay(); } - canvas.drawBitmap(mImage, mDisplayMatrix, new Paint()); + } - if (mDisplayMatrix.mapRect(imageBounds)) { - CropDrawingUtils.drawCropRect(canvas, imageBounds); - CropDrawingUtils.drawRuleOfThird(canvas, imageBounds); - CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize, imageBounds, - mCropObj.isFixedAspect(), mCropObj.getSelectState()); - } + public RectF getCrop() { + return mCropObj.getInnerBounds(); + } + + public RectF getPhoto() { + return mCropObj.getOuterBounds(); } @Override @@ -133,8 +133,6 @@ public class CropView extends View { mPrevX = x; mPrevY = y; mState = Mode.MOVE; - } else { - reset(); } break; case (MotionEvent.ACTION_UP): @@ -144,8 +142,6 @@ public class CropView extends View { mPrevX = x; mPrevY = y; mState = Mode.NONE; - } else { - reset(); } break; case (MotionEvent.ACTION_MOVE): @@ -155,41 +151,129 @@ public class CropView extends View { mCropObj.moveCurrentSelection(dx, dy); mPrevX = x; mPrevY = y; - } else { - reset(); } break; default: - reset(); break; } invalidate(); return true; } - public void reset() { - Log.w(LOGTAG, "reset called"); + private void reset() { + Log.w(LOGTAG, "crop reset called"); mState = Mode.NONE; mCropObj = null; + mRotation = 0; + mMovingBlock = false; + clearDisplay(); + } + + private void clearDisplay() { mDisplayMatrix = null; mDisplayMatrixInverse = null; - mMovingBlock = false; invalidate(); } - public boolean getCropBounds(RectF out_crop, RectF in_newContaining) { - Matrix m = new Matrix(); - RectF inner = mCropObj.getInnerBounds(); + protected void configChanged() { + mDirty = true; + } + + public void applyFreeAspect() { + mCropObj.unsetAspectRatio(); + invalidate(); + } + + public void applyOriginalAspect() { RectF outer = mCropObj.getOuterBounds(); - if (!m.setRectToRect(outer, in_newContaining, Matrix.ScaleToFit.FILL)) { - Log.w(LOGTAG, "failed to make transform matrix"); - return false; + if (!mCropObj.setInnerAspectRatio((int) outer.width(), (int) outer.height())) { + Log.w(LOGTAG, "failed to set aspect ratio original"); } - if (!m.mapRect(inner)) { - Log.w(LOGTAG, "failed to transform crop bounds"); - return false; + mCropObj.resetBoundsTo(outer, outer); + invalidate(); + } + + public void applySquareAspect() { + if (!mCropObj.setInnerAspectRatio(1, 1)) { + Log.w(LOGTAG, "failed to set aspect ratio square"); } - out_crop.set(inner); - return true; + invalidate(); + } + + @Override + public void onDraw(Canvas canvas) { + if (mBitmap == null) { + return; + } + if (mDirty) { + mDirty = false; + clearDisplay(); + } + + mImageBounds = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); + mScreenBounds = new RectF(0, 0, canvas.getWidth(), canvas.getHeight()); + mScreenBounds.inset(mMargin, mMargin); + + // If crop object doesn't exist, create it and update it from master + // state + if (mCropObj == null) { + reset(); + mCropObj = new CropObject(mImageBounds, mImageBounds, 0); + } + + // If display matrix doesn't exist, create it and its dependencies + if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { + mDisplayMatrix = new Matrix(); + mDisplayMatrix.reset(); + if (!CropDrawingUtils.setImageToScreenMatrix(mDisplayMatrix, mImageBounds, mScreenBounds, + mRotation)) { + Log.w(LOGTAG, "failed to get screen matrix"); + mDisplayMatrix = null; + return; + } + mDisplayMatrixInverse = new Matrix(); + mDisplayMatrixInverse.reset(); + if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) { + Log.w(LOGTAG, "could not invert display matrix"); + mDisplayMatrixInverse = null; + return; + } + // Scale min side and tolerance by display matrix scale factor + mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize)); + mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance)); + } + + mScreenImageBounds.set(mImageBounds); + + // Draw background shadow + if (mDisplayMatrix.mapRect(mScreenImageBounds)) { + int margin = (int) mDisplayMatrix.mapRadius(mShadowMargin); + mScreenImageBounds.roundOut(mShadowBounds); + mShadowBounds.set(mShadowBounds.left - margin, mShadowBounds.top - + margin, mShadowBounds.right + margin, mShadowBounds.bottom + margin); + mShadow.setBounds(mShadowBounds); + mShadow.draw(canvas); + } + + // Draw actual bitmap + canvas.drawBitmap(mBitmap, mDisplayMatrix, mPaint); + + mCropObj.getInnerBounds(mScreenCropBounds); + + if (mDisplayMatrix.mapRect(mScreenCropBounds)) { + + // Draw overlay shadows + Paint p = new Paint(); + p.setColor(mOverlayShadowColor); + p.setStyle(Paint.Style.FILL); + CropDrawingUtils.drawShadows(canvas, p, mScreenCropBounds, mScreenImageBounds); + + // Draw crop rect and markers + CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds); + CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds); + CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize, + mScreenCropBounds, mCropObj.isFixedAspect(), mCropObj.getSelectState()); + } + } } |