blob: 7e1572f0c0125a0275875c0613cbe439eb7c0884 [file] [log] [blame]
Owen Lina2fba682011-08-17 22:07:43 +08001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package com.android.gallery3d.app;
18
Ray Chen16e520e2011-10-11 12:49:45 +080019import android.app.ActionBar;
Owen Lina2fba682011-08-17 22:07:43 +080020import android.app.ProgressDialog;
21import android.app.WallpaperManager;
22import android.content.ContentValues;
23import android.content.Intent;
24import android.graphics.Bitmap;
Owen Linc1965fc2011-10-19 19:52:33 +080025import android.graphics.Bitmap.CompressFormat;
26import android.graphics.Bitmap.Config;
Owen Lina2fba682011-08-17 22:07:43 +080027import android.graphics.BitmapFactory;
28import android.graphics.BitmapRegionDecoder;
29import android.graphics.Canvas;
30import android.graphics.Paint;
31import android.graphics.Rect;
32import android.graphics.RectF;
Owen Lin3190f692011-09-02 21:15:42 +080033import android.media.ExifInterface;
Owen Lina2fba682011-08-17 22:07:43 +080034import android.net.Uri;
35import android.os.Bundle;
36import android.os.Environment;
37import android.os.Handler;
38import android.os.Message;
39import android.provider.MediaStore;
40import android.provider.MediaStore.Images;
Chih-Chung Chang4e051902012-02-11 07:19:47 +080041import android.util.FloatMath;
Owen Lina2fba682011-08-17 22:07:43 +080042import android.view.Menu;
43import android.view.MenuItem;
44import android.view.Window;
45import android.widget.Toast;
46
Owen Lin65e26532011-09-23 15:24:28 +080047import com.android.gallery3d.R;
48import com.android.gallery3d.common.BitmapUtils;
49import com.android.gallery3d.common.Utils;
50import com.android.gallery3d.data.DataManager;
51import com.android.gallery3d.data.LocalImage;
52import com.android.gallery3d.data.MediaItem;
53import com.android.gallery3d.data.MediaObject;
54import com.android.gallery3d.data.Path;
55import com.android.gallery3d.picasasource.PicasaSource;
56import com.android.gallery3d.ui.BitmapTileProvider;
57import com.android.gallery3d.ui.CropView;
58import com.android.gallery3d.ui.GLRoot;
59import com.android.gallery3d.ui.SynchronizedHandler;
60import com.android.gallery3d.ui.TileImageViewAdapter;
61import com.android.gallery3d.util.Future;
62import com.android.gallery3d.util.FutureListener;
63import com.android.gallery3d.util.GalleryUtils;
64import com.android.gallery3d.util.InterruptableOutputStream;
65import com.android.gallery3d.util.ThreadPool.CancelListener;
66import com.android.gallery3d.util.ThreadPool.Job;
67import com.android.gallery3d.util.ThreadPool.JobContext;
68
Owen Lina2fba682011-08-17 22:07:43 +080069import java.io.File;
70import java.io.FileNotFoundException;
71import java.io.FileOutputStream;
72import java.io.IOException;
73import java.io.OutputStream;
Chih-Chung Chang6156a862011-10-19 15:35:28 +080074import java.text.SimpleDateFormat;
75import java.util.Date;
Owen Lina2fba682011-08-17 22:07:43 +080076
77/**
78 * The activity can crop specific region of interest from an image.
79 */
80public class CropImage extends AbstractGalleryActivity {
81 private static final String TAG = "CropImage";
82 public static final String ACTION_CROP = "com.android.camera.action.CROP";
83
84 private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels
85 private static final int MAX_FILE_INDEX = 1000;
86 private static final int TILE_SIZE = 512;
87 private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600
88
89 private static final int MSG_LARGE_BITMAP = 1;
90 private static final int MSG_BITMAP = 2;
91 private static final int MSG_SAVE_COMPLETE = 3;
Owen Lin65e26532011-09-23 15:24:28 +080092 private static final int MSG_SHOW_SAVE_ERROR = 4;
Owen Lina2fba682011-08-17 22:07:43 +080093
94 private static final int MAX_BACKUP_IMAGE_SIZE = 320;
95 private static final int DEFAULT_COMPRESS_QUALITY = 90;
Chih-Chung Chang6156a862011-10-19 15:35:28 +080096 private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
97
98 // Change these to Images.Media.WIDTH/HEIGHT after they are unhidden.
99 private static final String WIDTH = "width";
100 private static final String HEIGHT = "height";
Owen Lina2fba682011-08-17 22:07:43 +0800101
102 public static final String KEY_RETURN_DATA = "return-data";
103 public static final String KEY_CROPPED_RECT = "cropped-rect";
104 public static final String KEY_ASPECT_X = "aspectX";
105 public static final String KEY_ASPECT_Y = "aspectY";
106 public static final String KEY_SPOTLIGHT_X = "spotlightX";
107 public static final String KEY_SPOTLIGHT_Y = "spotlightY";
108 public static final String KEY_OUTPUT_X = "outputX";
109 public static final String KEY_OUTPUT_Y = "outputY";
110 public static final String KEY_SCALE = "scale";
111 public static final String KEY_DATA = "data";
112 public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
113 public static final String KEY_OUTPUT_FORMAT = "outputFormat";
114 public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
115 public static final String KEY_NO_FACE_DETECTION = "noFaceDetection";
116
117 private static final String KEY_STATE = "state";
118
119 private static final int STATE_INIT = 0;
120 private static final int STATE_LOADED = 1;
121 private static final int STATE_SAVING = 2;
122
123 public static final String DOWNLOAD_STRING = "download";
124 public static final File DOWNLOAD_BUCKET = new File(
125 Environment.getExternalStorageDirectory(), DOWNLOAD_STRING);
126
127 public static final String CROP_ACTION = "com.android.camera.action.CROP";
128
129 private int mState = STATE_INIT;
130
131 private CropView mCropView;
132
133 private boolean mDoFaceDetection = true;
134
135 private Handler mMainHandler;
136
137 // We keep the following members so that we can free them
138
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;
146
147 private ProgressDialog mProgressDialog;
148 private Future<BitmapRegionDecoder> mLoadTask;
149 private Future<Bitmap> mLoadBitmapTask;
150 private Future<Intent> mSaveTask;
151
152 private MediaItem mMediaItem;
153
154 @Override
155 public void onCreate(Bundle bundle) {
156 super.onCreate(bundle);
157 requestWindowFeature(Window.FEATURE_ACTION_BAR);
158 requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
159
160 // Initialize UI
161 setContentView(R.layout.cropimage);
162 mCropView = new CropView(this);
163 getGLRoot().setContentPane(mCropView);
164
Ray Chen16e520e2011-10-11 12:49:45 +0800165 ActionBar actionBar = getActionBar();
166 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP,
167 ActionBar.DISPLAY_HOME_AS_UP);
168
Owen Lina2fba682011-08-17 22:07:43 +0800169 mMainHandler = new SynchronizedHandler(getGLRoot()) {
170 @Override
171 public void handleMessage(Message message) {
172 switch (message.what) {
173 case MSG_LARGE_BITMAP: {
174 mProgressDialog.dismiss();
175 onBitmapRegionDecoderAvailable((BitmapRegionDecoder) message.obj);
176 break;
177 }
178 case MSG_BITMAP: {
179 mProgressDialog.dismiss();
180 onBitmapAvailable((Bitmap) message.obj);
181 break;
182 }
Owen Lin65e26532011-09-23 15:24:28 +0800183 case MSG_SHOW_SAVE_ERROR: {
184 mProgressDialog.dismiss();
185 setResult(RESULT_CANCELED);
186 Toast.makeText(CropImage.this,
187 CropImage.this.getString(R.string.save_error),
188 Toast.LENGTH_LONG).show();
189 finish();
190 }
Owen Lina2fba682011-08-17 22:07:43 +0800191 case MSG_SAVE_COMPLETE: {
192 mProgressDialog.dismiss();
193 setResult(RESULT_OK, (Intent) message.obj);
194 finish();
195 break;
196 }
197 }
198 }
199 };
200
201 setCropParameters();
202 }
203
204 @Override
205 protected void onSaveInstanceState(Bundle saveState) {
206 saveState.putInt(KEY_STATE, mState);
207 }
208
209 @Override
210 public boolean onCreateOptionsMenu(Menu menu) {
211 super.onCreateOptionsMenu(menu);
212 getMenuInflater().inflate(R.menu.crop, menu);
213 return true;
214 }
215
216 @Override
217 public boolean onOptionsItemSelected(MenuItem item) {
218 switch (item.getItemId()) {
Ray Chen16e520e2011-10-11 12:49:45 +0800219 case android.R.id.home: {
220 finish();
221 break;
222 }
Owen Lina2fba682011-08-17 22:07:43 +0800223 case R.id.cancel: {
224 setResult(RESULT_CANCELED);
225 finish();
226 break;
227 }
228 case R.id.save: {
229 onSaveClicked();
230 break;
231 }
232 }
233 return true;
234 }
235
236 private class SaveOutput implements Job<Intent> {
Ray Chen16e520e2011-10-11 12:49:45 +0800237 private final RectF mCropRect;
Owen Lina2fba682011-08-17 22:07:43 +0800238
239 public SaveOutput(RectF cropRect) {
240 mCropRect = cropRect;
241 }
242
243 public Intent run(JobContext jc) {
244 RectF cropRect = mCropRect;
245 Bundle extra = getIntent().getExtras();
246
247 Rect rect = new Rect(
248 Math.round(cropRect.left), Math.round(cropRect.top),
249 Math.round(cropRect.right), Math.round(cropRect.bottom));
250
251 Intent result = new Intent();
252 result.putExtra(KEY_CROPPED_RECT, rect);
253 Bitmap cropped = null;
254 boolean outputted = false;
255 if (extra != null) {
256 Uri uri = (Uri) extra.getParcelable(MediaStore.EXTRA_OUTPUT);
257 if (uri != null) {
258 if (jc.isCancelled()) return null;
259 outputted = true;
260 cropped = getCroppedImage(rect);
261 if (!saveBitmapToUri(jc, cropped, uri)) return null;
262 }
263 if (extra.getBoolean(KEY_RETURN_DATA, false)) {
264 if (jc.isCancelled()) return null;
265 outputted = true;
266 if (cropped == null) cropped = getCroppedImage(rect);
267 result.putExtra(KEY_DATA, cropped);
268 }
269 if (extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) {
270 if (jc.isCancelled()) return null;
271 outputted = true;
272 if (cropped == null) cropped = getCroppedImage(rect);
273 if (!setAsWallpaper(jc, cropped)) return null;
274 }
275 }
276 if (!outputted) {
277 if (jc.isCancelled()) return null;
278 if (cropped == null) cropped = getCroppedImage(rect);
279 Uri data = saveToMediaProvider(jc, cropped);
280 if (data != null) result.setData(data);
281 }
282 return result;
283 }
284 }
285
286 public static String determineCompressFormat(MediaObject obj) {
287 String compressFormat = "JPEG";
288 if (obj instanceof MediaItem) {
289 String mime = ((MediaItem) obj).getMimeType();
290 if (mime.contains("png") || mime.contains("gif")) {
291 // Set the compress format to PNG for png and gif images
292 // because they may contain alpha values.
293 compressFormat = "PNG";
294 }
295 }
296 return compressFormat;
297 }
298
299 private boolean setAsWallpaper(JobContext jc, Bitmap wallpaper) {
300 try {
301 WallpaperManager.getInstance(this).setBitmap(wallpaper);
302 } catch (IOException e) {
303 Log.w(TAG, "fail to set wall paper", e);
304 }
305 return true;
306 }
307
308 private File saveMedia(
309 JobContext jc, Bitmap cropped, File directory, String filename) {
310 // Try file-1.jpg, file-2.jpg, ... until we find a filename
311 // which does not exist yet.
312 File candidate = null;
313 String fileExtension = getFileExtension();
314 for (int i = 1; i < MAX_FILE_INDEX; ++i) {
315 candidate = new File(directory, filename + "-" + i + "."
316 + fileExtension);
317 try {
318 if (candidate.createNewFile()) break;
319 } catch (IOException e) {
320 Log.e(TAG, "fail to create new file: "
321 + candidate.getAbsolutePath(), e);
322 return null;
323 }
324 }
325 if (!candidate.exists() || !candidate.isFile()) {
326 throw new RuntimeException("cannot create file: " + filename);
327 }
328
329 candidate.setReadable(true, false);
330 candidate.setWritable(true, false);
331
332 try {
333 FileOutputStream fos = new FileOutputStream(candidate);
334 try {
335 saveBitmapToOutputStream(jc, cropped,
336 convertExtensionToCompressFormat(fileExtension), fos);
337 } finally {
338 fos.close();
339 }
340 } catch (IOException e) {
341 Log.e(TAG, "fail to save image: "
342 + candidate.getAbsolutePath(), e);
343 candidate.delete();
344 return null;
345 }
346
347 if (jc.isCancelled()) {
348 candidate.delete();
349 return null;
350 }
351
352 return candidate;
353 }
354
355 private Uri saveToMediaProvider(JobContext jc, Bitmap cropped) {
356 if (PicasaSource.isPicasaImage(mMediaItem)) {
357 return savePicasaImage(jc, cropped);
358 } else if (mMediaItem instanceof LocalImage) {
359 return saveLocalImage(jc, cropped);
360 } else {
Chih-Chung Chang6156a862011-10-19 15:35:28 +0800361 return saveGenericImage(jc, cropped);
Owen Lina2fba682011-08-17 22:07:43 +0800362 }
363 }
364
365 private Uri savePicasaImage(JobContext jc, Bitmap cropped) {
366 if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
367 throw new RuntimeException("cannot create download folder");
368 }
369
370 String filename = PicasaSource.getImageTitle(mMediaItem);
371 int pos = filename.lastIndexOf('.');
372 if (pos >= 0) filename = filename.substring(0, pos);
373 File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
374 if (output == null) return null;
375
Owen Lin3190f692011-09-02 21:15:42 +0800376 copyExif(mMediaItem, output.getAbsolutePath(), cropped.getWidth(), cropped.getHeight());
377
Owen Lina2fba682011-08-17 22:07:43 +0800378 long now = System.currentTimeMillis() / 1000;
379 ContentValues values = new ContentValues();
380 values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem));
381 values.put(Images.Media.DISPLAY_NAME, output.getName());
382 values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem));
383 values.put(Images.Media.DATE_MODIFIED, now);
384 values.put(Images.Media.DATE_ADDED, now);
Chih-Chung Chang6156a862011-10-19 15:35:28 +0800385 values.put(Images.Media.MIME_TYPE, getOutputMimeType());
Owen Lina2fba682011-08-17 22:07:43 +0800386 values.put(Images.Media.ORIENTATION, 0);
387 values.put(Images.Media.DATA, output.getAbsolutePath());
388 values.put(Images.Media.SIZE, output.length());
Chih-Chung Chang6156a862011-10-19 15:35:28 +0800389 values.put(WIDTH, cropped.getWidth());
390 values.put(HEIGHT, cropped.getHeight());
Owen Lina2fba682011-08-17 22:07:43 +0800391
392 double latitude = PicasaSource.getLatitude(mMediaItem);
393 double longitude = PicasaSource.getLongitude(mMediaItem);
394 if (GalleryUtils.isValidLocation(latitude, longitude)) {
395 values.put(Images.Media.LATITUDE, latitude);
396 values.put(Images.Media.LONGITUDE, longitude);
397 }
398 return getContentResolver().insert(
399 Images.Media.EXTERNAL_CONTENT_URI, values);
400 }
401
402 private Uri saveLocalImage(JobContext jc, Bitmap cropped) {
403 LocalImage localImage = (LocalImage) mMediaItem;
404
405 File oldPath = new File(localImage.filePath);
406 File directory = new File(oldPath.getParent());
407
408 String filename = oldPath.getName();
409 int pos = filename.lastIndexOf('.');
410 if (pos >= 0) filename = filename.substring(0, pos);
411 File output = saveMedia(jc, cropped, directory, filename);
412 if (output == null) return null;
413
Owen Lin3190f692011-09-02 21:15:42 +0800414 copyExif(oldPath.getAbsolutePath(), output.getAbsolutePath(),
415 cropped.getWidth(), cropped.getHeight());
416
Owen Lina2fba682011-08-17 22:07:43 +0800417 long now = System.currentTimeMillis() / 1000;
418 ContentValues values = new ContentValues();
419 values.put(Images.Media.TITLE, localImage.caption);
420 values.put(Images.Media.DISPLAY_NAME, output.getName());
421 values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs);
422 values.put(Images.Media.DATE_MODIFIED, now);
423 values.put(Images.Media.DATE_ADDED, now);
Chih-Chung Chang6156a862011-10-19 15:35:28 +0800424 values.put(Images.Media.MIME_TYPE, getOutputMimeType());
Owen Lina2fba682011-08-17 22:07:43 +0800425 values.put(Images.Media.ORIENTATION, 0);
426 values.put(Images.Media.DATA, output.getAbsolutePath());
427 values.put(Images.Media.SIZE, output.length());
Chih-Chung Chang6156a862011-10-19 15:35:28 +0800428 values.put(WIDTH, cropped.getWidth());
429 values.put(HEIGHT, cropped.getHeight());
Owen Lina2fba682011-08-17 22:07:43 +0800430
431 if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) {
432 values.put(Images.Media.LATITUDE, localImage.latitude);
433 values.put(Images.Media.LONGITUDE, localImage.longitude);
434 }
435 return getContentResolver().insert(
436 Images.Media.EXTERNAL_CONTENT_URI, values);
437 }
438
Chih-Chung Chang6156a862011-10-19 15:35:28 +0800439 private Uri saveGenericImage(JobContext jc, Bitmap cropped) {
440 if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
441 throw new RuntimeException("cannot create download folder");
442 }
443
444 long now = System.currentTimeMillis();
445 String filename = new SimpleDateFormat(TIME_STAMP_NAME).
446 format(new Date(now));
447
448 File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
449 if (output == null) return null;
450
451 ContentValues values = new ContentValues();
452 values.put(Images.Media.TITLE, filename);
453 values.put(Images.Media.DISPLAY_NAME, output.getName());
454 values.put(Images.Media.DATE_TAKEN, now);
455 values.put(Images.Media.DATE_MODIFIED, now / 1000);
456 values.put(Images.Media.DATE_ADDED, now / 1000);
457 values.put(Images.Media.MIME_TYPE, getOutputMimeType());
458 values.put(Images.Media.ORIENTATION, 0);
459 values.put(Images.Media.DATA, output.getAbsolutePath());
460 values.put(Images.Media.SIZE, output.length());
461 values.put(WIDTH, cropped.getWidth());
462 values.put(HEIGHT, cropped.getHeight());
463
464 return getContentResolver().insert(
465 Images.Media.EXTERNAL_CONTENT_URI, values);
466 }
467
Owen Lina2fba682011-08-17 22:07:43 +0800468 private boolean saveBitmapToOutputStream(
469 JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) {
470 // We wrap the OutputStream so that it can be interrupted.
471 final InterruptableOutputStream ios = new InterruptableOutputStream(os);
472 jc.setCancelListener(new CancelListener() {
473 public void onCancel() {
474 ios.interrupt();
475 }
476 });
477 try {
478 bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os);
Owen Lin65e26532011-09-23 15:24:28 +0800479 return !jc.isCancelled();
Owen Lina2fba682011-08-17 22:07:43 +0800480 } finally {
481 jc.setCancelListener(null);
482 Utils.closeSilently(os);
483 }
Owen Lina2fba682011-08-17 22:07:43 +0800484 }
485
486 private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) {
487 try {
488 return saveBitmapToOutputStream(jc, bitmap,
489 convertExtensionToCompressFormat(getFileExtension()),
490 getContentResolver().openOutputStream(uri));
491 } catch (FileNotFoundException e) {
492 Log.w(TAG, "cannot write output", e);
493 }
494 return true;
495 }
496
497 private CompressFormat convertExtensionToCompressFormat(String extension) {
498 return extension.equals("png")
499 ? CompressFormat.PNG
500 : CompressFormat.JPEG;
501 }
502
Chih-Chung Chang6156a862011-10-19 15:35:28 +0800503 private String getOutputMimeType() {
504 return getFileExtension().equals("png") ? "image/png" : "image/jpeg";
505 }
506
Owen Lina2fba682011-08-17 22:07:43 +0800507 private String getFileExtension() {
508 String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT);
509 String outputFormat = (requestFormat == null)
510 ? determineCompressFormat(mMediaItem)
511 : requestFormat;
512
513 outputFormat = outputFormat.toLowerCase();
514 return (outputFormat.equals("png") || outputFormat.equals("gif"))
515 ? "png" // We don't support gif compression.
516 : "jpg";
517 }
518
519 private void onSaveClicked() {
520 Bundle extra = getIntent().getExtras();
521 RectF cropRect = mCropView.getCropRectangle();
522 if (cropRect == null) return;
523 mState = STATE_SAVING;
524 int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER)
525 ? R.string.wallpaper
526 : R.string.saving_image;
527 mProgressDialog = ProgressDialog.show(
528 this, null, getString(messageId), true, false);
529 mSaveTask = getThreadPool().submit(new SaveOutput(cropRect),
530 new FutureListener<Intent>() {
531 public void onFutureDone(Future<Intent> future) {
532 mSaveTask = null;
Owen Lin65e26532011-09-23 15:24:28 +0800533 if (future.isCancelled()) return;
534 Intent intent = future.get();
535 if (intent != null) {
536 mMainHandler.sendMessage(mMainHandler.obtainMessage(
537 MSG_SAVE_COMPLETE, intent));
538 } else {
539 mMainHandler.sendEmptyMessage(MSG_SHOW_SAVE_ERROR);
540 }
Owen Lina2fba682011-08-17 22:07:43 +0800541 }
542 });
543 }
544
545 private Bitmap getCroppedImage(Rect rect) {
546 Utils.assertTrue(rect.width() > 0 && rect.height() > 0);
547
548 Bundle extras = getIntent().getExtras();
549 // (outputX, outputY) = the width and height of the returning bitmap.
550 int outputX = rect.width();
551 int outputY = rect.height();
552 if (extras != null) {
553 outputX = extras.getInt(KEY_OUTPUT_X, outputX);
554 outputY = extras.getInt(KEY_OUTPUT_Y, outputY);
555 }
556
557 if (outputX * outputY > MAX_PIXEL_COUNT) {
Chih-Chung Chang4e051902012-02-11 07:19:47 +0800558 float scale = FloatMath.sqrt((float) MAX_PIXEL_COUNT / outputX / outputY);
Owen Lina2fba682011-08-17 22:07:43 +0800559 Log.w(TAG, "scale down the cropped image: " + scale);
560 outputX = Math.round(scale * outputX);
561 outputY = Math.round(scale * outputY);
562 }
563
564 // (rect.width() * scaleX, rect.height() * scaleY) =
565 // the size of drawing area in output bitmap
566 float scaleX = 1;
567 float scaleY = 1;
568 Rect dest = new Rect(0, 0, outputX, outputY);
569 if (extras == null || extras.getBoolean(KEY_SCALE, true)) {
570 scaleX = (float) outputX / rect.width();
571 scaleY = (float) outputY / rect.height();
572 if (extras == null || !extras.getBoolean(
573 KEY_SCALE_UP_IF_NEEDED, false)) {
574 if (scaleX > 1f) scaleX = 1;
575 if (scaleY > 1f) scaleY = 1;
576 }
577 }
578
579 // Keep the content in the center (or crop the content)
580 int rectWidth = Math.round(rect.width() * scaleX);
581 int rectHeight = Math.round(rect.height() * scaleY);
582 dest.set(Math.round((outputX - rectWidth) / 2f),
583 Math.round((outputY - rectHeight) / 2f),
584 Math.round((outputX + rectWidth) / 2f),
585 Math.round((outputY + rectHeight) / 2f));
586
587 if (mBitmapInIntent != null) {
588 Bitmap source = mBitmapInIntent;
589 Bitmap result = Bitmap.createBitmap(
590 outputX, outputY, Config.ARGB_8888);
591 Canvas canvas = new Canvas(result);
592 canvas.drawBitmap(source, rect, dest, null);
593 return result;
594 }
595
Owen Lina2fba682011-08-17 22:07:43 +0800596 if (mUseRegionDecoder) {
Owen Linc1965fc2011-10-19 19:52:33 +0800597 int rotation = mMediaItem.getFullImageRotation();
598 rotateRectangle(rect, mCropView.getImageWidth(),
599 mCropView.getImageHeight(), 360 - rotation);
600 rotateRectangle(dest, outputX, outputY, 360 - rotation);
601
Owen Lina2fba682011-08-17 22:07:43 +0800602 BitmapFactory.Options options = new BitmapFactory.Options();
603 int sample = BitmapUtils.computeSampleSizeLarger(
604 Math.max(scaleX, scaleY));
605 options.inSampleSize = sample;
606 if ((rect.width() / sample) == dest.width()
607 && (rect.height() / sample) == dest.height()
608 && rotation == 0) {
609 // To prevent concurrent access in GLThread
610 synchronized (mRegionDecoder) {
611 return mRegionDecoder.decodeRegion(rect, options);
612 }
613 }
614 Bitmap result = Bitmap.createBitmap(
615 outputX, outputY, Config.ARGB_8888);
616 Canvas canvas = new Canvas(result);
617 rotateCanvas(canvas, outputX, outputY, rotation);
618 drawInTiles(canvas, mRegionDecoder, rect, dest, sample);
619 return result;
620 } else {
Owen Linc1965fc2011-10-19 19:52:33 +0800621 int rotation = mMediaItem.getRotation();
622 rotateRectangle(rect, mCropView.getImageWidth(),
623 mCropView.getImageHeight(), 360 - rotation);
624 rotateRectangle(dest, outputX, outputY, 360 - rotation);
Owen Lina2fba682011-08-17 22:07:43 +0800625 Bitmap result = Bitmap.createBitmap(outputX, outputY, Config.ARGB_8888);
626 Canvas canvas = new Canvas(result);
627 rotateCanvas(canvas, outputX, outputY, rotation);
628 canvas.drawBitmap(mBitmap,
629 rect, dest, new Paint(Paint.FILTER_BITMAP_FLAG));
630 return result;
631 }
632 }
633
634 private static void rotateCanvas(
635 Canvas canvas, int width, int height, int rotation) {
636 canvas.translate(width / 2, height / 2);
637 canvas.rotate(rotation);
638 if (((rotation / 90) & 0x01) == 0) {
639 canvas.translate(-width / 2, -height / 2);
640 } else {
641 canvas.translate(-height / 2, -width / 2);
642 }
643 }
644
645 private static void rotateRectangle(
646 Rect rect, int width, int height, int rotation) {
647 if (rotation == 0 || rotation == 360) return;
648
649 int w = rect.width();
650 int h = rect.height();
651 switch (rotation) {
652 case 90: {
653 rect.top = rect.left;
654 rect.left = height - rect.bottom;
655 rect.right = rect.left + h;
656 rect.bottom = rect.top + w;
657 return;
658 }
659 case 180: {
660 rect.left = width - rect.right;
661 rect.top = height - rect.bottom;
662 rect.right = rect.left + w;
663 rect.bottom = rect.top + h;
664 return;
665 }
666 case 270: {
667 rect.left = rect.top;
668 rect.top = width - rect.right;
669 rect.right = rect.left + h;
670 rect.bottom = rect.top + w;
671 return;
672 }
673 default: throw new AssertionError();
674 }
675 }
676
677 private void drawInTiles(Canvas canvas,
678 BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample) {
679 int tileSize = TILE_SIZE * sample;
680 Rect tileRect = new Rect();
681 BitmapFactory.Options options = new BitmapFactory.Options();
682 options.inPreferredConfig = Config.ARGB_8888;
683 options.inSampleSize = sample;
684 canvas.translate(dest.left, dest.top);
685 canvas.scale((float) sample * dest.width() / rect.width(),
686 (float) sample * dest.height() / rect.height());
687 Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
688 for (int tx = rect.left, x = 0;
689 tx < rect.right; tx += tileSize, x += TILE_SIZE) {
690 for (int ty = rect.top, y = 0;
691 ty < rect.bottom; ty += tileSize, y += TILE_SIZE) {
692 tileRect.set(tx, ty, tx + tileSize, ty + tileSize);
693 if (tileRect.intersect(rect)) {
694 Bitmap bitmap;
695
696 // To prevent concurrent access in GLThread
697 synchronized (decoder) {
698 bitmap = decoder.decodeRegion(tileRect, options);
699 }
700 canvas.drawBitmap(bitmap, x, y, paint);
701 bitmap.recycle();
702 }
703 }
704 }
705 }
706
707 private void onBitmapRegionDecoderAvailable(
708 BitmapRegionDecoder regionDecoder) {
709
710 if (regionDecoder == null) {
Chih-Chung Chang52da9b72012-02-01 18:40:57 +0800711 Toast.makeText(this, R.string.fail_to_load_image, Toast.LENGTH_SHORT).show();
Owen Lina2fba682011-08-17 22:07:43 +0800712 finish();
713 return;
714 }
715 mRegionDecoder = regionDecoder;
716 mUseRegionDecoder = true;
717 mState = STATE_LOADED;
718
719 BitmapFactory.Options options = new BitmapFactory.Options();
720 int width = regionDecoder.getWidth();
721 int height = regionDecoder.getHeight();
722 options.inSampleSize = BitmapUtils.computeSampleSize(width, height,
723 BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT);
724 mBitmap = regionDecoder.decodeRegion(
725 new Rect(0, 0, width, height), options);
726 mCropView.setDataModel(new TileImageViewAdapter(
Owen Linc1965fc2011-10-19 19:52:33 +0800727 mBitmap, regionDecoder), mMediaItem.getFullImageRotation());
Owen Lina2fba682011-08-17 22:07:43 +0800728 if (mDoFaceDetection) {
729 mCropView.detectFaces(mBitmap);
730 } else {
731 mCropView.initializeHighlightRectangle();
732 }
733 }
734
735 private void onBitmapAvailable(Bitmap bitmap) {
736 if (bitmap == null) {
Chih-Chung Chang52da9b72012-02-01 18:40:57 +0800737 Toast.makeText(this, R.string.fail_to_load_image, Toast.LENGTH_SHORT).show();
Owen Lina2fba682011-08-17 22:07:43 +0800738 finish();
739 return;
740 }
741 mUseRegionDecoder = false;
742 mState = STATE_LOADED;
743
744 mBitmap = bitmap;
745 BitmapFactory.Options options = new BitmapFactory.Options();
746 mCropView.setDataModel(new BitmapTileProvider(bitmap, 512),
747 mMediaItem.getRotation());
748 if (mDoFaceDetection) {
749 mCropView.detectFaces(bitmap);
750 } else {
751 mCropView.initializeHighlightRectangle();
752 }
753 }
754
755 private void setCropParameters() {
756 Bundle extras = getIntent().getExtras();
757 if (extras == null)
758 return;
759 int aspectX = extras.getInt(KEY_ASPECT_X, 0);
760 int aspectY = extras.getInt(KEY_ASPECT_Y, 0);
761 if (aspectX != 0 && aspectY != 0) {
762 mCropView.setAspectRatio((float) aspectX / aspectY);
763 }
764
765 float spotlightX = extras.getFloat(KEY_SPOTLIGHT_X, 0);
766 float spotlightY = extras.getFloat(KEY_SPOTLIGHT_Y, 0);
767 if (spotlightX != 0 && spotlightY != 0) {
768 mCropView.setSpotlightRatio(spotlightX, spotlightY);
769 }
770 }
771
772 private void initializeData() {
773 Bundle extras = getIntent().getExtras();
774
775 if (extras != null) {
776 if (extras.containsKey(KEY_NO_FACE_DETECTION)) {
777 mDoFaceDetection = !extras.getBoolean(KEY_NO_FACE_DETECTION);
778 }
779
780 mBitmapInIntent = extras.getParcelable(KEY_DATA);
781
782 if (mBitmapInIntent != null) {
783 mBitmapTileProvider =
784 new BitmapTileProvider(mBitmapInIntent, MAX_BACKUP_IMAGE_SIZE);
785 mCropView.setDataModel(mBitmapTileProvider, 0);
786 if (mDoFaceDetection) {
787 mCropView.detectFaces(mBitmapInIntent);
788 } else {
789 mCropView.initializeHighlightRectangle();
790 }
791 mState = STATE_LOADED;
792 return;
793 }
794 }
795
796 mProgressDialog = ProgressDialog.show(
797 this, null, getString(R.string.loading_image), true, false);
798
799 mMediaItem = getMediaItemFromIntentData();
800 if (mMediaItem == null) return;
801
802 boolean supportedByBitmapRegionDecoder =
803 (mMediaItem.getSupportedOperations() & MediaItem.SUPPORT_FULL_IMAGE) != 0;
804 if (supportedByBitmapRegionDecoder) {
805 mLoadTask = getThreadPool().submit(new LoadDataTask(mMediaItem),
806 new FutureListener<BitmapRegionDecoder>() {
807 public void onFutureDone(Future<BitmapRegionDecoder> future) {
808 mLoadTask = null;
809 BitmapRegionDecoder decoder = future.get();
810 if (future.isCancelled()) {
811 if (decoder != null) decoder.recycle();
812 return;
813 }
814 mMainHandler.sendMessage(mMainHandler.obtainMessage(
815 MSG_LARGE_BITMAP, decoder));
816 }
817 });
818 } else {
819 mLoadBitmapTask = getThreadPool().submit(new LoadBitmapDataTask(mMediaItem),
820 new FutureListener<Bitmap>() {
821 public void onFutureDone(Future<Bitmap> future) {
822 mLoadBitmapTask = null;
823 Bitmap bitmap = future.get();
824 if (future.isCancelled()) {
825 if (bitmap != null) bitmap.recycle();
826 return;
827 }
828 mMainHandler.sendMessage(mMainHandler.obtainMessage(
829 MSG_BITMAP, bitmap));
830 }
831 });
832 }
833 }
834
835 @Override
836 protected void onResume() {
837 super.onResume();
838 if (mState == STATE_INIT) initializeData();
839 if (mState == STATE_SAVING) onSaveClicked();
840
841 // TODO: consider to do it in GLView system
842 GLRoot root = getGLRoot();
843 root.lockRenderThread();
844 try {
845 mCropView.resume();
846 } finally {
847 root.unlockRenderThread();
848 }
849 }
850
851 @Override
852 protected void onPause() {
853 super.onPause();
854
855 Future<BitmapRegionDecoder> loadTask = mLoadTask;
856 if (loadTask != null && !loadTask.isDone()) {
857 // load in progress, try to cancel it
858 loadTask.cancel();
859 loadTask.waitDone();
860 mProgressDialog.dismiss();
861 }
862
863 Future<Bitmap> loadBitmapTask = mLoadBitmapTask;
864 if (loadBitmapTask != null && !loadBitmapTask.isDone()) {
865 // load in progress, try to cancel it
866 loadBitmapTask.cancel();
867 loadBitmapTask.waitDone();
868 mProgressDialog.dismiss();
869 }
870
871 Future<Intent> saveTask = mSaveTask;
872 if (saveTask != null && !saveTask.isDone()) {
873 // save in progress, try to cancel it
874 saveTask.cancel();
875 saveTask.waitDone();
876 mProgressDialog.dismiss();
877 }
878 GLRoot root = getGLRoot();
879 root.lockRenderThread();
880 try {
881 mCropView.pause();
882 } finally {
883 root.unlockRenderThread();
884 }
885 }
886
887 private MediaItem getMediaItemFromIntentData() {
888 Uri uri = getIntent().getData();
889 DataManager manager = getDataManager();
890 if (uri == null) {
891 Log.w(TAG, "no data given");
892 return null;
893 }
894 Path path = manager.findPathByUri(uri);
895 if (path == null) {
896 Log.w(TAG, "cannot get path for: " + uri);
897 return null;
898 }
899 return (MediaItem) manager.getMediaObject(path);
900 }
901
902 private class LoadDataTask implements Job<BitmapRegionDecoder> {
903 MediaItem mItem;
904
905 public LoadDataTask(MediaItem item) {
906 mItem = item;
907 }
908
909 public BitmapRegionDecoder run(JobContext jc) {
910 return mItem == null ? null : mItem.requestLargeImage().run(jc);
911 }
912 }
913
914 private class LoadBitmapDataTask implements Job<Bitmap> {
915 MediaItem mItem;
916
917 public LoadBitmapDataTask(MediaItem item) {
918 mItem = item;
919 }
920 public Bitmap run(JobContext jc) {
921 return mItem == null
922 ? null
923 : mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
924 }
925 }
Owen Lin3190f692011-09-02 21:15:42 +0800926
927 private static final String[] EXIF_TAGS = {
928 ExifInterface.TAG_DATETIME,
929 ExifInterface.TAG_MAKE,
930 ExifInterface.TAG_MODEL,
931 ExifInterface.TAG_FLASH,
932 ExifInterface.TAG_GPS_LATITUDE,
933 ExifInterface.TAG_GPS_LONGITUDE,
934 ExifInterface.TAG_GPS_LATITUDE_REF,
935 ExifInterface.TAG_GPS_LONGITUDE_REF,
936 ExifInterface.TAG_GPS_ALTITUDE,
937 ExifInterface.TAG_GPS_ALTITUDE_REF,
938 ExifInterface.TAG_GPS_TIMESTAMP,
939 ExifInterface.TAG_GPS_DATESTAMP,
940 ExifInterface.TAG_WHITE_BALANCE,
941 ExifInterface.TAG_FOCAL_LENGTH,
942 ExifInterface.TAG_GPS_PROCESSING_METHOD};
943
944 private static void copyExif(MediaItem item, String destination, int newWidth, int newHeight) {
945 try {
946 ExifInterface newExif = new ExifInterface(destination);
947 PicasaSource.extractExifValues(item, newExif);
948 newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
949 newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
950 newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
951 newExif.saveAttributes();
952 } catch (Throwable t) {
953 Log.w(TAG, "cannot copy exif: " + item, t);
954 }
955 }
956
957 private static void copyExif(String source, String destination, int newWidth, int newHeight) {
958 try {
959 ExifInterface oldExif = new ExifInterface(source);
960 ExifInterface newExif = new ExifInterface(destination);
961
962 newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
963 newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
964 newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
965
966 for (String tag : EXIF_TAGS) {
967 String value = oldExif.getAttribute(tag);
968 if (value != null) {
969 newExif.setAttribute(tag, value);
970 }
971 }
972
973 // Handle some special values here
974 String value = oldExif.getAttribute(ExifInterface.TAG_APERTURE);
975 if (value != null) {
976 try {
977 float aperture = Float.parseFloat(value);
978 newExif.setAttribute(ExifInterface.TAG_APERTURE,
979 String.valueOf((int) (aperture * 10 + 0.5f)) + "/10");
980 } catch (NumberFormatException e) {
981 Log.w(TAG, "cannot parse aperture: " + value);
982 }
983 }
984
985 // TODO: The code is broken, need to fix the JHEAD lib
986 /*
987 value = oldExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
988 if (value != null) {
989 try {
990 double exposure = Double.parseDouble(value);
991 testToRational("test exposure", exposure);
992 newExif.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, value);
993 } catch (NumberFormatException e) {
994 Log.w(TAG, "cannot parse exposure time: " + value);
995 }
996 }
997
998 value = oldExif.getAttribute(ExifInterface.TAG_ISO);
999 if (value != null) {
1000 try {
1001 int iso = Integer.parseInt(value);
1002 newExif.setAttribute(ExifInterface.TAG_ISO, String.valueOf(iso) + "/1");
1003 } catch (NumberFormatException e) {
1004 Log.w(TAG, "cannot parse exposure time: " + value);
1005 }
1006 }*/
1007 newExif.saveAttributes();
1008 } catch (Throwable t) {
1009 Log.w(TAG, "cannot copy exif: " + source, t);
1010 }
1011 }
Owen Lina2fba682011-08-17 22:07:43 +08001012}