2 * Copyright (C) 2010 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.gallery3d.app;
19 import android.annotation.TargetApi;
20 import android.app.ActionBar;
21 import android.app.ProgressDialog;
22 import android.app.WallpaperManager;
23 import android.content.ContentValues;
24 import android.content.Intent;
25 import android.graphics.Bitmap;
26 import android.graphics.Bitmap.CompressFormat;
27 import android.graphics.Bitmap.Config;
28 import android.graphics.BitmapFactory;
29 import android.graphics.BitmapRegionDecoder;
30 import android.graphics.Canvas;
31 import android.graphics.Paint;
32 import android.graphics.Rect;
33 import android.graphics.RectF;
34 import android.media.ExifInterface;
35 import android.net.Uri;
36 import android.os.Build;
37 import android.os.Bundle;
38 import android.os.Environment;
39 import android.os.Handler;
40 import android.os.Message;
41 import android.provider.MediaStore;
42 import android.provider.MediaStore.Images;
43 import android.util.FloatMath;
44 import android.view.Menu;
45 import android.view.MenuItem;
46 import android.view.Window;
47 import android.widget.Toast;
49 import com.android.gallery3d.R;
50 import com.android.gallery3d.common.ApiHelper;
51 import com.android.gallery3d.common.BitmapUtils;
52 import com.android.gallery3d.common.ExifTags;
53 import com.android.gallery3d.common.Utils;
54 import com.android.gallery3d.data.DataManager;
55 import com.android.gallery3d.data.LocalImage;
56 import com.android.gallery3d.data.MediaItem;
57 import com.android.gallery3d.data.MediaObject;
58 import com.android.gallery3d.data.Path;
59 import com.android.gallery3d.picasasource.PicasaSource;
60 import com.android.gallery3d.ui.BitmapTileProvider;
61 import com.android.gallery3d.ui.CropView;
62 import com.android.gallery3d.ui.GLRoot;
63 import com.android.gallery3d.ui.SynchronizedHandler;
64 import com.android.gallery3d.ui.TileImageViewAdapter;
65 import com.android.gallery3d.util.BucketNames;
66 import com.android.gallery3d.util.Future;
67 import com.android.gallery3d.util.FutureListener;
68 import com.android.gallery3d.util.GalleryUtils;
69 import com.android.gallery3d.util.InterruptableOutputStream;
70 import com.android.gallery3d.util.ThreadPool.CancelListener;
71 import com.android.gallery3d.util.ThreadPool.Job;
72 import com.android.gallery3d.util.ThreadPool.JobContext;
75 import java.io.FileNotFoundException;
76 import java.io.FileOutputStream;
77 import java.io.IOException;
78 import java.io.OutputStream;
79 import java.text.SimpleDateFormat;
80 import java.util.Date;
83 * The activity can crop specific region of interest from an image.
85 public class CropImage extends AbstractGalleryActivity {
86 private static final String TAG = "CropImage";
87 public static final String ACTION_CROP = "com.android.camera.action.CROP";
89 private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels
90 private static final int MAX_FILE_INDEX = 1000;
91 private static final int TILE_SIZE = 512;
92 private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600
94 private static final int MSG_LARGE_BITMAP = 1;
95 private static final int MSG_BITMAP = 2;
96 private static final int MSG_SAVE_COMPLETE = 3;
97 private static final int MSG_SHOW_SAVE_ERROR = 4;
99 private static final int MAX_BACKUP_IMAGE_SIZE = 320;
100 private static final int DEFAULT_COMPRESS_QUALITY = 90;
101 private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
103 public static final String KEY_RETURN_DATA = "return-data";
104 public static final String KEY_CROPPED_RECT = "cropped-rect";
105 public static final String KEY_ASPECT_X = "aspectX";
106 public static final String KEY_ASPECT_Y = "aspectY";
107 public static final String KEY_SPOTLIGHT_X = "spotlightX";
108 public static final String KEY_SPOTLIGHT_Y = "spotlightY";
109 public static final String KEY_OUTPUT_X = "outputX";
110 public static final String KEY_OUTPUT_Y = "outputY";
111 public static final String KEY_SCALE = "scale";
112 public static final String KEY_DATA = "data";
113 public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
114 public static final String KEY_OUTPUT_FORMAT = "outputFormat";
115 public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
116 public static final String KEY_NO_FACE_DETECTION = "noFaceDetection";
118 private static final String KEY_STATE = "state";
120 private static final int STATE_INIT = 0;
121 private static final int STATE_LOADED = 1;
122 private static final int STATE_SAVING = 2;
124 public static final File DOWNLOAD_BUCKET = new File(
125 Environment.getExternalStorageDirectory(), BucketNames.DOWNLOAD);
127 public static final String CROP_ACTION = "com.android.camera.action.CROP";
129 private int mState = STATE_INIT;
131 private CropView mCropView;
133 private boolean mDoFaceDetection = true;
135 private Handler mMainHandler;
137 // We keep the following members so that we can free them
139 // mBitmap is the unrotated bitmap we pass in to mCropView for detect faces.
140 // mCropView is responsible for rotating it to the way that it is viewed by users.
141 private Bitmap mBitmap;
142 private BitmapTileProvider mBitmapTileProvider;
143 private BitmapRegionDecoder mRegionDecoder;
144 private Bitmap mBitmapInIntent;
145 private boolean mUseRegionDecoder = false;
147 private ProgressDialog mProgressDialog;
148 private Future<BitmapRegionDecoder> mLoadTask;
149 private Future<Bitmap> mLoadBitmapTask;
150 private Future<Intent> mSaveTask;
152 private MediaItem mMediaItem;
155 public void onCreate(Bundle bundle) {
156 super.onCreate(bundle);
157 requestWindowFeature(Window.FEATURE_ACTION_BAR);
158 requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
161 setContentView(R.layout.cropimage);
162 mCropView = new CropView(this);
163 getGLRoot().setContentPane(mCropView);
165 ActionBar actionBar = getActionBar();
166 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE,
167 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE);
168 Bundle extra = getIntent().getExtras();
169 if (extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) {
170 actionBar.setTitle(R.string.set_wallpaper);
173 mMainHandler = new SynchronizedHandler(getGLRoot()) {
175 public void handleMessage(Message message) {
176 switch (message.what) {
177 case MSG_LARGE_BITMAP: {
178 mProgressDialog.dismiss();
179 onBitmapRegionDecoderAvailable((BitmapRegionDecoder) message.obj);
183 mProgressDialog.dismiss();
184 onBitmapAvailable((Bitmap) message.obj);
187 case MSG_SHOW_SAVE_ERROR: {
188 mProgressDialog.dismiss();
189 setResult(RESULT_CANCELED);
190 Toast.makeText(CropImage.this,
191 CropImage.this.getString(R.string.save_error),
192 Toast.LENGTH_LONG).show();
195 case MSG_SAVE_COMPLETE: {
196 mProgressDialog.dismiss();
197 setResult(RESULT_OK, (Intent) message.obj);
209 protected void onSaveInstanceState(Bundle saveState) {
210 saveState.putInt(KEY_STATE, mState);
214 public boolean onCreateOptionsMenu(Menu menu) {
215 super.onCreateOptionsMenu(menu);
216 getMenuInflater().inflate(R.menu.crop, menu);
221 public boolean onOptionsItemSelected(MenuItem item) {
222 switch (item.getItemId()) {
223 case android.R.id.home: {
228 setResult(RESULT_CANCELED);
241 public void onBackPressed() {
245 private class SaveOutput implements Job<Intent> {
246 private final RectF mCropRect;
248 public SaveOutput(RectF cropRect) {
249 mCropRect = cropRect;
252 public Intent run(JobContext jc) {
253 RectF cropRect = mCropRect;
254 Bundle extra = getIntent().getExtras();
256 Rect rect = new Rect(
257 Math.round(cropRect.left), Math.round(cropRect.top),
258 Math.round(cropRect.right), Math.round(cropRect.bottom));
260 Intent result = new Intent();
261 result.putExtra(KEY_CROPPED_RECT, rect);
262 Bitmap cropped = null;
263 boolean outputted = false;
265 Uri uri = (Uri) extra.getParcelable(MediaStore.EXTRA_OUTPUT);
267 if (jc.isCancelled()) return null;
269 cropped = getCroppedImage(rect);
270 if (!saveBitmapToUri(jc, cropped, uri)) return null;
272 if (extra.getBoolean(KEY_RETURN_DATA, false)) {
273 if (jc.isCancelled()) return null;
275 if (cropped == null) cropped = getCroppedImage(rect);
276 result.putExtra(KEY_DATA, cropped);
278 if (extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) {
279 if (jc.isCancelled()) return null;
281 if (cropped == null) cropped = getCroppedImage(rect);
282 if (!setAsWallpaper(jc, cropped)) return null;
286 if (jc.isCancelled()) return null;
287 if (cropped == null) cropped = getCroppedImage(rect);
288 Uri data = saveToMediaProvider(jc, cropped);
289 if (data != null) result.setData(data);
295 public static String determineCompressFormat(MediaObject obj) {
296 String compressFormat = "JPEG";
297 if (obj instanceof MediaItem) {
298 String mime = ((MediaItem) obj).getMimeType();
299 if (mime.contains("png") || mime.contains("gif")) {
300 // Set the compress format to PNG for png and gif images
301 // because they may contain alpha values.
302 compressFormat = "PNG";
305 return compressFormat;
308 private boolean setAsWallpaper(JobContext jc, Bitmap wallpaper) {
310 WallpaperManager.getInstance(this).setBitmap(wallpaper);
311 } catch (IOException e) {
312 Log.w(TAG, "fail to set wall paper", e);
317 private File saveMedia(
318 JobContext jc, Bitmap cropped, File directory, String filename) {
319 // Try file-1.jpg, file-2.jpg, ... until we find a filename
320 // which does not exist yet.
321 File candidate = null;
322 String fileExtension = getFileExtension();
323 for (int i = 1; i < MAX_FILE_INDEX; ++i) {
324 candidate = new File(directory, filename + "-" + i + "."
327 if (candidate.createNewFile()) break;
328 } catch (IOException e) {
329 Log.e(TAG, "fail to create new file: "
330 + candidate.getAbsolutePath(), e);
334 if (!candidate.exists() || !candidate.isFile()) {
335 throw new RuntimeException("cannot create file: " + filename);
338 candidate.setReadable(true, false);
339 candidate.setWritable(true, false);
342 FileOutputStream fos = new FileOutputStream(candidate);
344 saveBitmapToOutputStream(jc, cropped,
345 convertExtensionToCompressFormat(fileExtension), fos);
349 } catch (IOException e) {
350 Log.e(TAG, "fail to save image: "
351 + candidate.getAbsolutePath(), e);
356 if (jc.isCancelled()) {
364 private Uri saveToMediaProvider(JobContext jc, Bitmap cropped) {
365 if (PicasaSource.isPicasaImage(mMediaItem)) {
366 return savePicasaImage(jc, cropped);
367 } else if (mMediaItem instanceof LocalImage) {
368 return saveLocalImage(jc, cropped);
370 return saveGenericImage(jc, cropped);
374 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
375 private static void setImageSize(ContentValues values, int width, int height) {
376 // The two fields are available since ICS but got published in JB
377 if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
378 values.put(Images.Media.WIDTH, width);
379 values.put(Images.Media.HEIGHT, height);
383 private Uri savePicasaImage(JobContext jc, Bitmap cropped) {
384 if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
385 throw new RuntimeException("cannot create download folder");
388 String filename = PicasaSource.getImageTitle(mMediaItem);
389 int pos = filename.lastIndexOf('.');
390 if (pos >= 0) filename = filename.substring(0, pos);
391 File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
392 if (output == null) return null;
394 copyExif(mMediaItem, output.getAbsolutePath(), cropped.getWidth(), cropped.getHeight());
396 long now = System.currentTimeMillis() / 1000;
397 ContentValues values = new ContentValues();
398 values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem));
399 values.put(Images.Media.DISPLAY_NAME, output.getName());
400 values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem));
401 values.put(Images.Media.DATE_MODIFIED, now);
402 values.put(Images.Media.DATE_ADDED, now);
403 values.put(Images.Media.MIME_TYPE, getOutputMimeType());
404 values.put(Images.Media.ORIENTATION, 0);
405 values.put(Images.Media.DATA, output.getAbsolutePath());
406 values.put(Images.Media.SIZE, output.length());
407 setImageSize(values, cropped.getWidth(), cropped.getHeight());
409 double latitude = PicasaSource.getLatitude(mMediaItem);
410 double longitude = PicasaSource.getLongitude(mMediaItem);
411 if (GalleryUtils.isValidLocation(latitude, longitude)) {
412 values.put(Images.Media.LATITUDE, latitude);
413 values.put(Images.Media.LONGITUDE, longitude);
415 return getContentResolver().insert(
416 Images.Media.EXTERNAL_CONTENT_URI, values);
419 private Uri saveLocalImage(JobContext jc, Bitmap cropped) {
420 LocalImage localImage = (LocalImage) mMediaItem;
422 File oldPath = new File(localImage.filePath);
423 File directory = new File(oldPath.getParent());
425 String filename = oldPath.getName();
426 int pos = filename.lastIndexOf('.');
427 if (pos >= 0) filename = filename.substring(0, pos);
428 File output = saveMedia(jc, cropped, directory, filename);
429 if (output == null) return null;
431 copyExif(oldPath.getAbsolutePath(), output.getAbsolutePath(),
432 cropped.getWidth(), cropped.getHeight());
434 long now = System.currentTimeMillis() / 1000;
435 ContentValues values = new ContentValues();
436 values.put(Images.Media.TITLE, localImage.caption);
437 values.put(Images.Media.DISPLAY_NAME, output.getName());
438 values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs);
439 values.put(Images.Media.DATE_MODIFIED, now);
440 values.put(Images.Media.DATE_ADDED, now);
441 values.put(Images.Media.MIME_TYPE, getOutputMimeType());
442 values.put(Images.Media.ORIENTATION, 0);
443 values.put(Images.Media.DATA, output.getAbsolutePath());
444 values.put(Images.Media.SIZE, output.length());
446 setImageSize(values, cropped.getWidth(), cropped.getHeight());
448 if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) {
449 values.put(Images.Media.LATITUDE, localImage.latitude);
450 values.put(Images.Media.LONGITUDE, localImage.longitude);
452 return getContentResolver().insert(
453 Images.Media.EXTERNAL_CONTENT_URI, values);
456 private Uri saveGenericImage(JobContext jc, Bitmap cropped) {
457 if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
458 throw new RuntimeException("cannot create download folder");
461 long now = System.currentTimeMillis();
462 String filename = new SimpleDateFormat(TIME_STAMP_NAME).
463 format(new Date(now));
465 File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
466 if (output == null) return null;
468 ContentValues values = new ContentValues();
469 values.put(Images.Media.TITLE, filename);
470 values.put(Images.Media.DISPLAY_NAME, output.getName());
471 values.put(Images.Media.DATE_TAKEN, now);
472 values.put(Images.Media.DATE_MODIFIED, now / 1000);
473 values.put(Images.Media.DATE_ADDED, now / 1000);
474 values.put(Images.Media.MIME_TYPE, getOutputMimeType());
475 values.put(Images.Media.ORIENTATION, 0);
476 values.put(Images.Media.DATA, output.getAbsolutePath());
477 values.put(Images.Media.SIZE, output.length());
479 setImageSize(values, cropped.getWidth(), cropped.getHeight());
481 return getContentResolver().insert(
482 Images.Media.EXTERNAL_CONTENT_URI, values);
485 private boolean saveBitmapToOutputStream(
486 JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) {
487 // We wrap the OutputStream so that it can be interrupted.
488 final InterruptableOutputStream ios = new InterruptableOutputStream(os);
489 jc.setCancelListener(new CancelListener() {
490 public void onCancel() {
495 bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os);
496 return !jc.isCancelled();
498 jc.setCancelListener(null);
499 Utils.closeSilently(os);
503 private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) {
505 return saveBitmapToOutputStream(jc, bitmap,
506 convertExtensionToCompressFormat(getFileExtension()),
507 getContentResolver().openOutputStream(uri));
508 } catch (FileNotFoundException e) {
509 Log.w(TAG, "cannot write output", e);
514 private CompressFormat convertExtensionToCompressFormat(String extension) {
515 return extension.equals("png")
517 : CompressFormat.JPEG;
520 private String getOutputMimeType() {
521 return getFileExtension().equals("png") ? "image/png" : "image/jpeg";
524 private String getFileExtension() {
525 String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT);
526 String outputFormat = (requestFormat == null)
527 ? determineCompressFormat(mMediaItem)
530 outputFormat = outputFormat.toLowerCase();
531 return (outputFormat.equals("png") || outputFormat.equals("gif"))
532 ? "png" // We don't support gif compression.
536 private void onSaveClicked() {
537 Bundle extra = getIntent().getExtras();
538 RectF cropRect = mCropView.getCropRectangle();
539 if (cropRect == null) return;
540 mState = STATE_SAVING;
541 int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER)
543 : R.string.saving_image;
544 mProgressDialog = ProgressDialog.show(
545 this, null, getString(messageId), true, false);
546 mSaveTask = getThreadPool().submit(new SaveOutput(cropRect),
547 new FutureListener<Intent>() {
548 public void onFutureDone(Future<Intent> future) {
550 if (future.isCancelled()) return;
551 Intent intent = future.get();
552 if (intent != null) {
553 mMainHandler.sendMessage(mMainHandler.obtainMessage(
554 MSG_SAVE_COMPLETE, intent));
556 mMainHandler.sendEmptyMessage(MSG_SHOW_SAVE_ERROR);
562 private Bitmap getCroppedImage(Rect rect) {
563 Utils.assertTrue(rect.width() > 0 && rect.height() > 0);
565 Bundle extras = getIntent().getExtras();
566 // (outputX, outputY) = the width and height of the returning bitmap.
567 int outputX = rect.width();
568 int outputY = rect.height();
569 if (extras != null) {
570 outputX = extras.getInt(KEY_OUTPUT_X, outputX);
571 outputY = extras.getInt(KEY_OUTPUT_Y, outputY);
574 if (outputX * outputY > MAX_PIXEL_COUNT) {
575 float scale = FloatMath.sqrt((float) MAX_PIXEL_COUNT / outputX / outputY);
576 Log.w(TAG, "scale down the cropped image: " + scale);
577 outputX = Math.round(scale * outputX);
578 outputY = Math.round(scale * outputY);
581 // (rect.width() * scaleX, rect.height() * scaleY) =
582 // the size of drawing area in output bitmap
585 Rect dest = new Rect(0, 0, outputX, outputY);
586 if (extras == null || extras.getBoolean(KEY_SCALE, true)) {
587 scaleX = (float) outputX / rect.width();
588 scaleY = (float) outputY / rect.height();
589 if (extras == null || !extras.getBoolean(
590 KEY_SCALE_UP_IF_NEEDED, false)) {
591 if (scaleX > 1f) scaleX = 1;
592 if (scaleY > 1f) scaleY = 1;
596 // Keep the content in the center (or crop the content)
597 int rectWidth = Math.round(rect.width() * scaleX);
598 int rectHeight = Math.round(rect.height() * scaleY);
599 dest.set(Math.round((outputX - rectWidth) / 2f),
600 Math.round((outputY - rectHeight) / 2f),
601 Math.round((outputX + rectWidth) / 2f),
602 Math.round((outputY + rectHeight) / 2f));
604 if (mBitmapInIntent != null) {
605 Bitmap source = mBitmapInIntent;
606 Bitmap result = Bitmap.createBitmap(
607 outputX, outputY, Config.ARGB_8888);
608 Canvas canvas = new Canvas(result);
609 canvas.drawBitmap(source, rect, dest, null);
613 if (mUseRegionDecoder) {
614 int rotation = mMediaItem.getFullImageRotation();
615 rotateRectangle(rect, mCropView.getImageWidth(),
616 mCropView.getImageHeight(), 360 - rotation);
617 rotateRectangle(dest, outputX, outputY, 360 - rotation);
619 BitmapFactory.Options options = new BitmapFactory.Options();
620 int sample = BitmapUtils.computeSampleSizeLarger(
621 Math.max(scaleX, scaleY));
622 options.inSampleSize = sample;
624 // The decoding result is what we want if
625 // 1. The size of the decoded bitmap match the destination's size
626 // 2. The destination covers the whole output bitmap
628 if ((rect.width() / sample) == dest.width()
629 && (rect.height() / sample) == dest.height()
630 && (outputX == dest.width()) && (outputY == dest.height())
632 // To prevent concurrent access in GLThread
633 synchronized (mRegionDecoder) {
634 return mRegionDecoder.decodeRegion(rect, options);
637 Bitmap result = Bitmap.createBitmap(
638 outputX, outputY, Config.ARGB_8888);
639 Canvas canvas = new Canvas(result);
640 rotateCanvas(canvas, outputX, outputY, rotation);
641 drawInTiles(canvas, mRegionDecoder, rect, dest, sample);
644 int rotation = mMediaItem.getRotation();
645 rotateRectangle(rect, mCropView.getImageWidth(),
646 mCropView.getImageHeight(), 360 - rotation);
647 rotateRectangle(dest, outputX, outputY, 360 - rotation);
648 Bitmap result = Bitmap.createBitmap(outputX, outputY, Config.ARGB_8888);
649 Canvas canvas = new Canvas(result);
650 rotateCanvas(canvas, outputX, outputY, rotation);
651 canvas.drawBitmap(mBitmap,
652 rect, dest, new Paint(Paint.FILTER_BITMAP_FLAG));
657 private static void rotateCanvas(
658 Canvas canvas, int width, int height, int rotation) {
659 canvas.translate(width / 2, height / 2);
660 canvas.rotate(rotation);
661 if (((rotation / 90) & 0x01) == 0) {
662 canvas.translate(-width / 2, -height / 2);
664 canvas.translate(-height / 2, -width / 2);
668 private static void rotateRectangle(
669 Rect rect, int width, int height, int rotation) {
670 if (rotation == 0 || rotation == 360) return;
672 int w = rect.width();
673 int h = rect.height();
676 rect.top = rect.left;
677 rect.left = height - rect.bottom;
678 rect.right = rect.left + h;
679 rect.bottom = rect.top + w;
683 rect.left = width - rect.right;
684 rect.top = height - rect.bottom;
685 rect.right = rect.left + w;
686 rect.bottom = rect.top + h;
690 rect.left = rect.top;
691 rect.top = width - rect.right;
692 rect.right = rect.left + h;
693 rect.bottom = rect.top + w;
696 default: throw new AssertionError();
700 private void drawInTiles(Canvas canvas,
701 BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample) {
702 int tileSize = TILE_SIZE * sample;
703 Rect tileRect = new Rect();
704 BitmapFactory.Options options = new BitmapFactory.Options();
705 options.inPreferredConfig = Config.ARGB_8888;
706 options.inSampleSize = sample;
707 canvas.translate(dest.left, dest.top);
708 canvas.scale((float) sample * dest.width() / rect.width(),
709 (float) sample * dest.height() / rect.height());
710 Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
711 for (int tx = rect.left, x = 0;
712 tx < rect.right; tx += tileSize, x += TILE_SIZE) {
713 for (int ty = rect.top, y = 0;
714 ty < rect.bottom; ty += tileSize, y += TILE_SIZE) {
715 tileRect.set(tx, ty, tx + tileSize, ty + tileSize);
716 if (tileRect.intersect(rect)) {
719 // To prevent concurrent access in GLThread
720 synchronized (decoder) {
721 bitmap = decoder.decodeRegion(tileRect, options);
723 canvas.drawBitmap(bitmap, x, y, paint);
730 private void onBitmapRegionDecoderAvailable(
731 BitmapRegionDecoder regionDecoder) {
733 if (regionDecoder == null) {
734 Toast.makeText(this, R.string.fail_to_load_image, Toast.LENGTH_SHORT).show();
738 mRegionDecoder = regionDecoder;
739 mUseRegionDecoder = true;
740 mState = STATE_LOADED;
742 BitmapFactory.Options options = new BitmapFactory.Options();
743 int width = regionDecoder.getWidth();
744 int height = regionDecoder.getHeight();
745 options.inSampleSize = BitmapUtils.computeSampleSize(width, height,
746 BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT);
747 mBitmap = regionDecoder.decodeRegion(
748 new Rect(0, 0, width, height), options);
749 mCropView.setDataModel(new TileImageViewAdapter(
750 mBitmap, regionDecoder), mMediaItem.getFullImageRotation());
751 if (mDoFaceDetection) {
752 mCropView.detectFaces(mBitmap);
754 mCropView.initializeHighlightRectangle();
758 private void onBitmapAvailable(Bitmap bitmap) {
759 if (bitmap == null) {
760 Toast.makeText(this, R.string.fail_to_load_image, Toast.LENGTH_SHORT).show();
764 mUseRegionDecoder = false;
765 mState = STATE_LOADED;
768 BitmapFactory.Options options = new BitmapFactory.Options();
769 mCropView.setDataModel(new BitmapTileProvider(bitmap, 512),
770 mMediaItem.getRotation());
771 if (mDoFaceDetection) {
772 mCropView.detectFaces(bitmap);
774 mCropView.initializeHighlightRectangle();
778 private void setCropParameters() {
779 Bundle extras = getIntent().getExtras();
782 int aspectX = extras.getInt(KEY_ASPECT_X, 0);
783 int aspectY = extras.getInt(KEY_ASPECT_Y, 0);
784 if (aspectX != 0 && aspectY != 0) {
785 mCropView.setAspectRatio((float) aspectX / aspectY);
788 float spotlightX = extras.getFloat(KEY_SPOTLIGHT_X, 0);
789 float spotlightY = extras.getFloat(KEY_SPOTLIGHT_Y, 0);
790 if (spotlightX != 0 && spotlightY != 0) {
791 mCropView.setSpotlightRatio(spotlightX, spotlightY);
795 private void initializeData() {
796 Bundle extras = getIntent().getExtras();
798 if (extras != null) {
799 if (extras.containsKey(KEY_NO_FACE_DETECTION)) {
800 mDoFaceDetection = !extras.getBoolean(KEY_NO_FACE_DETECTION);
803 mBitmapInIntent = extras.getParcelable(KEY_DATA);
805 if (mBitmapInIntent != null) {
806 mBitmapTileProvider =
807 new BitmapTileProvider(mBitmapInIntent, MAX_BACKUP_IMAGE_SIZE);
808 mCropView.setDataModel(mBitmapTileProvider, 0);
809 if (mDoFaceDetection) {
810 mCropView.detectFaces(mBitmapInIntent);
812 mCropView.initializeHighlightRectangle();
814 mState = STATE_LOADED;
819 mProgressDialog = ProgressDialog.show(
820 this, null, getString(R.string.loading_image), true, false);
822 mMediaItem = getMediaItemFromIntentData();
823 if (mMediaItem == null) return;
825 boolean supportedByBitmapRegionDecoder =
826 (mMediaItem.getSupportedOperations() & MediaItem.SUPPORT_FULL_IMAGE) != 0;
827 if (supportedByBitmapRegionDecoder) {
828 mLoadTask = getThreadPool().submit(new LoadDataTask(mMediaItem),
829 new FutureListener<BitmapRegionDecoder>() {
830 public void onFutureDone(Future<BitmapRegionDecoder> future) {
832 BitmapRegionDecoder decoder = future.get();
833 if (future.isCancelled()) {
834 if (decoder != null) decoder.recycle();
837 mMainHandler.sendMessage(mMainHandler.obtainMessage(
838 MSG_LARGE_BITMAP, decoder));
842 mLoadBitmapTask = getThreadPool().submit(new LoadBitmapDataTask(mMediaItem),
843 new FutureListener<Bitmap>() {
844 public void onFutureDone(Future<Bitmap> future) {
845 mLoadBitmapTask = null;
846 Bitmap bitmap = future.get();
847 if (future.isCancelled()) {
848 if (bitmap != null) bitmap.recycle();
851 mMainHandler.sendMessage(mMainHandler.obtainMessage(
852 MSG_BITMAP, bitmap));
859 protected void onResume() {
861 if (mState == STATE_INIT) initializeData();
862 if (mState == STATE_SAVING) onSaveClicked();
864 // TODO: consider to do it in GLView system
865 GLRoot root = getGLRoot();
866 root.lockRenderThread();
870 root.unlockRenderThread();
875 protected void onPause() {
878 Future<BitmapRegionDecoder> loadTask = mLoadTask;
879 if (loadTask != null && !loadTask.isDone()) {
880 // load in progress, try to cancel it
883 mProgressDialog.dismiss();
886 Future<Bitmap> loadBitmapTask = mLoadBitmapTask;
887 if (loadBitmapTask != null && !loadBitmapTask.isDone()) {
888 // load in progress, try to cancel it
889 loadBitmapTask.cancel();
890 loadBitmapTask.waitDone();
891 mProgressDialog.dismiss();
894 Future<Intent> saveTask = mSaveTask;
895 if (saveTask != null && !saveTask.isDone()) {
896 // save in progress, try to cancel it
899 mProgressDialog.dismiss();
901 GLRoot root = getGLRoot();
902 root.lockRenderThread();
906 root.unlockRenderThread();
910 private MediaItem getMediaItemFromIntentData() {
911 Uri uri = getIntent().getData();
912 DataManager manager = getDataManager();
913 Path path = manager.findPathByUri(uri, getIntent().getType());
915 Log.w(TAG, "cannot get path for: " + uri + ", or no data given");
918 return (MediaItem) manager.getMediaObject(path);
921 private class LoadDataTask implements Job<BitmapRegionDecoder> {
924 public LoadDataTask(MediaItem item) {
928 public BitmapRegionDecoder run(JobContext jc) {
929 return mItem == null ? null : mItem.requestLargeImage().run(jc);
933 private class LoadBitmapDataTask implements Job<Bitmap> {
936 public LoadBitmapDataTask(MediaItem item) {
939 public Bitmap run(JobContext jc) {
942 : mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
946 private static final String[] EXIF_TAGS = {
947 ExifInterface.TAG_DATETIME,
948 ExifInterface.TAG_MAKE,
949 ExifInterface.TAG_MODEL,
950 ExifInterface.TAG_FLASH,
951 ExifInterface.TAG_GPS_LATITUDE,
952 ExifInterface.TAG_GPS_LONGITUDE,
953 ExifInterface.TAG_GPS_LATITUDE_REF,
954 ExifInterface.TAG_GPS_LONGITUDE_REF,
955 ExifInterface.TAG_GPS_ALTITUDE,
956 ExifInterface.TAG_GPS_ALTITUDE_REF,
957 ExifInterface.TAG_GPS_TIMESTAMP,
958 ExifInterface.TAG_GPS_DATESTAMP,
959 ExifInterface.TAG_WHITE_BALANCE,
960 ExifInterface.TAG_FOCAL_LENGTH,
961 ExifInterface.TAG_GPS_PROCESSING_METHOD};
963 private static void copyExif(MediaItem item, String destination, int newWidth, int newHeight) {
965 ExifInterface newExif = new ExifInterface(destination);
966 PicasaSource.extractExifValues(item, newExif);
967 newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
968 newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
969 newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
970 newExif.saveAttributes();
971 } catch (Throwable t) {
972 Log.w(TAG, "cannot copy exif: " + item, t);
976 private static void copyExif(String source, String destination, int newWidth, int newHeight) {
978 ExifInterface oldExif = new ExifInterface(source);
979 ExifInterface newExif = new ExifInterface(destination);
981 newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
982 newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
983 newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
985 for (String tag : EXIF_TAGS) {
986 String value = oldExif.getAttribute(tag);
988 newExif.setAttribute(tag, value);
992 // Handle some special values here
993 String value = oldExif.getAttribute(ExifTags.TAG_APERTURE);
996 float aperture = Float.parseFloat(value);
997 newExif.setAttribute(ExifTags.TAG_APERTURE,
998 String.valueOf((int) (aperture * 10 + 0.5f)) + "/10");
999 } catch (NumberFormatException e) {
1000 Log.w(TAG, "cannot parse aperture: " + value);
1004 // TODO: The code is broken, need to fix the JHEAD lib
1006 value = oldExif.getAttribute(ExifTags.TAG_EXPOSURE_TIME);
1007 if (value != null) {
1009 double exposure = Double.parseDouble(value);
1010 testToRational("test exposure", exposure);
1011 newExif.setAttribute(ExifTags.TAG_EXPOSURE_TIME, value);
1012 } catch (NumberFormatException e) {
1013 Log.w(TAG, "cannot parse exposure time: " + value);
1017 value = oldExif.getAttribute(ExifTags.TAG_ISO);
1018 if (value != null) {
1020 int iso = Integer.parseInt(value);
1021 newExif.setAttribute(ExifTags.TAG_ISO, String.valueOf(iso) + "/1");
1022 } catch (NumberFormatException e) {
1023 Log.w(TAG, "cannot parse exposure time: " + value);
1026 newExif.saveAttributes();
1027 } catch (Throwable t) {
1028 Log.w(TAG, "cannot copy exif: " + source, t);