1 /* 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 17 package com.android.gallery3d.app; 18 19 import android.app.ActionBar; 20 import android.app.ProgressDialog; 21 import android.app.WallpaperManager; 22 import android.content.ContentValues; 23 import android.content.Intent; 24 import android.graphics.Bitmap; 25 import android.graphics.Bitmap.CompressFormat; 26 import android.graphics.Bitmap.Config; 27 import android.graphics.BitmapFactory; 28 import android.graphics.BitmapRegionDecoder; 29 import android.graphics.Canvas; 30 import android.graphics.Paint; 31 import android.graphics.Rect; 32 import android.graphics.RectF; 33 import android.media.ExifInterface; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.os.Environment; 37 import android.os.Handler; 38 import android.os.Message; 39 import android.provider.MediaStore; 40 import android.provider.MediaStore.Images; 41 import android.view.Menu; 42 import android.view.MenuItem; 43 import android.view.Window; 44 import android.widget.Toast; 45 46 import com.android.gallery3d.R; 47 import com.android.gallery3d.common.BitmapUtils; 48 import com.android.gallery3d.common.Utils; 49 import com.android.gallery3d.data.DataManager; 50 import com.android.gallery3d.data.LocalImage; 51 import com.android.gallery3d.data.MediaItem; 52 import com.android.gallery3d.data.MediaObject; 53 import com.android.gallery3d.data.Path; 54 import com.android.gallery3d.picasasource.PicasaSource; 55 import com.android.gallery3d.ui.BitmapTileProvider; 56 import com.android.gallery3d.ui.CropView; 57 import com.android.gallery3d.ui.GLRoot; 58 import com.android.gallery3d.ui.SynchronizedHandler; 59 import com.android.gallery3d.ui.TileImageViewAdapter; 60 import com.android.gallery3d.util.Future; 61 import com.android.gallery3d.util.FutureListener; 62 import com.android.gallery3d.util.GalleryUtils; 63 import com.android.gallery3d.util.InterruptableOutputStream; 64 import com.android.gallery3d.util.ThreadPool.CancelListener; 65 import com.android.gallery3d.util.ThreadPool.Job; 66 import com.android.gallery3d.util.ThreadPool.JobContext; 67 68 import java.io.File; 69 import java.io.FileNotFoundException; 70 import java.io.FileOutputStream; 71 import java.io.IOException; 72 import java.io.OutputStream; 73 import java.text.SimpleDateFormat; 74 import java.util.Date; 75 76 /** 77 * The activity can crop specific region of interest from an image. 78 */ 79 public class CropImage extends AbstractGalleryActivity { 80 private static final String TAG = "CropImage"; 81 public static final String ACTION_CROP = "com.android.camera.action.CROP"; 82 83 private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels 84 private static final int MAX_FILE_INDEX = 1000; 85 private static final int TILE_SIZE = 512; 86 private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600 87 88 private static final int MSG_LARGE_BITMAP = 1; 89 private static final int MSG_BITMAP = 2; 90 private static final int MSG_SAVE_COMPLETE = 3; 91 private static final int MSG_SHOW_SAVE_ERROR = 4; 92 93 private static final int MAX_BACKUP_IMAGE_SIZE = 320; 94 private static final int DEFAULT_COMPRESS_QUALITY = 90; 95 private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss"; 96 97 // Change these to Images.Media.WIDTH/HEIGHT after they are unhidden. 98 private static final String WIDTH = "width"; 99 private static final String HEIGHT = "height"; 100 101 public static final String KEY_RETURN_DATA = "return-data"; 102 public static final String KEY_CROPPED_RECT = "cropped-rect"; 103 public static final String KEY_ASPECT_X = "aspectX"; 104 public static final String KEY_ASPECT_Y = "aspectY"; 105 public static final String KEY_SPOTLIGHT_X = "spotlightX"; 106 public static final String KEY_SPOTLIGHT_Y = "spotlightY"; 107 public static final String KEY_OUTPUT_X = "outputX"; 108 public static final String KEY_OUTPUT_Y = "outputY"; 109 public static final String KEY_SCALE = "scale"; 110 public static final String KEY_DATA = "data"; 111 public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded"; 112 public static final String KEY_OUTPUT_FORMAT = "outputFormat"; 113 public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper"; 114 public static final String KEY_NO_FACE_DETECTION = "noFaceDetection"; 115 116 private static final String KEY_STATE = "state"; 117 118 private static final int STATE_INIT = 0; 119 private static final int STATE_LOADED = 1; 120 private static final int STATE_SAVING = 2; 121 122 public static final String DOWNLOAD_STRING = "download"; 123 public static final File DOWNLOAD_BUCKET = new File( 124 Environment.getExternalStorageDirectory(), DOWNLOAD_STRING); 125 126 public static final String CROP_ACTION = "com.android.camera.action.CROP"; 127 128 private int mState = STATE_INIT; 129 130 private CropView mCropView; 131 132 private boolean mDoFaceDetection = true; 133 134 private Handler mMainHandler; 135 136 // We keep the following members so that we can free them 137 138 // mBitmap is the unrotated bitmap we pass in to mCropView for detect faces. 139 // mCropView is responsible for rotating it to the way that it is viewed by users. 140 private Bitmap mBitmap; 141 private BitmapTileProvider mBitmapTileProvider; 142 private BitmapRegionDecoder mRegionDecoder; 143 private Bitmap mBitmapInIntent; 144 private boolean mUseRegionDecoder = false; 145 146 private ProgressDialog mProgressDialog; 147 private Future<BitmapRegionDecoder> mLoadTask; 148 private Future<Bitmap> mLoadBitmapTask; 149 private Future<Intent> mSaveTask; 150 151 private MediaItem mMediaItem; 152 153 @Override 154 public void onCreate(Bundle bundle) { 155 super.onCreate(bundle); 156 requestWindowFeature(Window.FEATURE_ACTION_BAR); 157 requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); 158 159 // Initialize UI 160 setContentView(R.layout.cropimage); 161 mCropView = new CropView(this); 162 getGLRoot().setContentPane(mCropView); 163 164 ActionBar actionBar = getActionBar(); 165 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, 166 ActionBar.DISPLAY_HOME_AS_UP); 167 168 mMainHandler = new SynchronizedHandler(getGLRoot()) { 169 @Override 170 public void handleMessage(Message message) { 171 switch (message.what) { 172 case MSG_LARGE_BITMAP: { 173 mProgressDialog.dismiss(); 174 onBitmapRegionDecoderAvailable((BitmapRegionDecoder) message.obj); 175 break; 176 } 177 case MSG_BITMAP: { 178 mProgressDialog.dismiss(); 179 onBitmapAvailable((Bitmap) message.obj); 180 break; 181 } 182 case MSG_SHOW_SAVE_ERROR: { 183 mProgressDialog.dismiss(); 184 setResult(RESULT_CANCELED); 185 Toast.makeText(CropImage.this, 186 CropImage.this.getString(R.string.save_error), 187 Toast.LENGTH_LONG).show(); 188 finish(); 189 } 190 case MSG_SAVE_COMPLETE: { 191 mProgressDialog.dismiss(); 192 setResult(RESULT_OK, (Intent) message.obj); 193 finish(); 194 break; 195 } 196 } 197 } 198 }; 199 200 setCropParameters(); 201 } 202 203 @Override 204 protected void onSaveInstanceState(Bundle saveState) { 205 saveState.putInt(KEY_STATE, mState); 206 } 207 208 @Override 209 public boolean onCreateOptionsMenu(Menu menu) { 210 super.onCreateOptionsMenu(menu); 211 getMenuInflater().inflate(R.menu.crop, menu); 212 return true; 213 } 214 215 @Override 216 public boolean onOptionsItemSelected(MenuItem item) { 217 switch (item.getItemId()) { 218 case android.R.id.home: { 219 finish(); 220 break; 221 } 222 case R.id.cancel: { 223 setResult(RESULT_CANCELED); 224 finish(); 225 break; 226 } 227 case R.id.save: { 228 onSaveClicked(); 229 break; 230 } 231 } 232 return true; 233 } 234 235 private class SaveOutput implements Job<Intent> { 236 private final RectF mCropRect; 237 238 public SaveOutput(RectF cropRect) { 239 mCropRect = cropRect; 240 } 241 242 public Intent run(JobContext jc) { 243 RectF cropRect = mCropRect; 244 Bundle extra = getIntent().getExtras(); 245 246 Rect rect = new Rect( 247 Math.round(cropRect.left), Math.round(cropRect.top), 248 Math.round(cropRect.right), Math.round(cropRect.bottom)); 249 250 Intent result = new Intent(); 251 result.putExtra(KEY_CROPPED_RECT, rect); 252 Bitmap cropped = null; 253 boolean outputted = false; 254 if (extra != null) { 255 Uri uri = (Uri) extra.getParcelable(MediaStore.EXTRA_OUTPUT); 256 if (uri != null) { 257 if (jc.isCancelled()) return null; 258 outputted = true; 259 cropped = getCroppedImage(rect); 260 if (!saveBitmapToUri(jc, cropped, uri)) return null; 261 } 262 if (extra.getBoolean(KEY_RETURN_DATA, false)) { 263 if (jc.isCancelled()) return null; 264 outputted = true; 265 if (cropped == null) cropped = getCroppedImage(rect); 266 result.putExtra(KEY_DATA, cropped); 267 } 268 if (extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) { 269 if (jc.isCancelled()) return null; 270 outputted = true; 271 if (cropped == null) cropped = getCroppedImage(rect); 272 if (!setAsWallpaper(jc, cropped)) return null; 273 } 274 } 275 if (!outputted) { 276 if (jc.isCancelled()) return null; 277 if (cropped == null) cropped = getCroppedImage(rect); 278 Uri data = saveToMediaProvider(jc, cropped); 279 if (data != null) result.setData(data); 280 } 281 return result; 282 } 283 } 284 285 public static String determineCompressFormat(MediaObject obj) { 286 String compressFormat = "JPEG"; 287 if (obj instanceof MediaItem) { 288 String mime = ((MediaItem) obj).getMimeType(); 289 if (mime.contains("png") || mime.contains("gif")) { 290 // Set the compress format to PNG for png and gif images 291 // because they may contain alpha values. 292 compressFormat = "PNG"; 293 } 294 } 295 return compressFormat; 296 } 297 298 private boolean setAsWallpaper(JobContext jc, Bitmap wallpaper) { 299 try { 300 WallpaperManager.getInstance(this).setBitmap(wallpaper); 301 } catch (IOException e) { 302 Log.w(TAG, "fail to set wall paper", e); 303 } 304 return true; 305 } 306 307 private File saveMedia( 308 JobContext jc, Bitmap cropped, File directory, String filename) { 309 // Try file-1.jpg, file-2.jpg, ... until we find a filename 310 // which does not exist yet. 311 File candidate = null; 312 String fileExtension = getFileExtension(); 313 for (int i = 1; i < MAX_FILE_INDEX; ++i) { 314 candidate = new File(directory, filename + "-" + i + "." 315 + fileExtension); 316 try { 317 if (candidate.createNewFile()) break; 318 } catch (IOException e) { 319 Log.e(TAG, "fail to create new file: " 320 + candidate.getAbsolutePath(), e); 321 return null; 322 } 323 } 324 if (!candidate.exists() || !candidate.isFile()) { 325 throw new RuntimeException("cannot create file: " + filename); 326 } 327 328 candidate.setReadable(true, false); 329 candidate.setWritable(true, false); 330 331 try { 332 FileOutputStream fos = new FileOutputStream(candidate); 333 try { 334 saveBitmapToOutputStream(jc, cropped, 335 convertExtensionToCompressFormat(fileExtension), fos); 336 } finally { 337 fos.close(); 338 } 339 } catch (IOException e) { 340 Log.e(TAG, "fail to save image: " 341 + candidate.getAbsolutePath(), e); 342 candidate.delete(); 343 return null; 344 } 345 346 if (jc.isCancelled()) { 347 candidate.delete(); 348 return null; 349 } 350 351 return candidate; 352 } 353 354 private Uri saveToMediaProvider(JobContext jc, Bitmap cropped) { 355 if (PicasaSource.isPicasaImage(mMediaItem)) { 356 return savePicasaImage(jc, cropped); 357 } else if (mMediaItem instanceof LocalImage) { 358 return saveLocalImage(jc, cropped); 359 } else { 360 return saveGenericImage(jc, cropped); 361 } 362 } 363 364 private Uri savePicasaImage(JobContext jc, Bitmap cropped) { 365 if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) { 366 throw new RuntimeException("cannot create download folder"); 367 } 368 369 String filename = PicasaSource.getImageTitle(mMediaItem); 370 int pos = filename.lastIndexOf('.'); 371 if (pos >= 0) filename = filename.substring(0, pos); 372 File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename); 373 if (output == null) return null; 374 375 copyExif(mMediaItem, output.getAbsolutePath(), cropped.getWidth(), cropped.getHeight()); 376 377 long now = System.currentTimeMillis() / 1000; 378 ContentValues values = new ContentValues(); 379 values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem)); 380 values.put(Images.Media.DISPLAY_NAME, output.getName()); 381 values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem)); 382 values.put(Images.Media.DATE_MODIFIED, now); 383 values.put(Images.Media.DATE_ADDED, now); 384 values.put(Images.Media.MIME_TYPE, getOutputMimeType()); 385 values.put(Images.Media.ORIENTATION, 0); 386 values.put(Images.Media.DATA, output.getAbsolutePath()); 387 values.put(Images.Media.SIZE, output.length()); 388 values.put(WIDTH, cropped.getWidth()); 389 values.put(HEIGHT, cropped.getHeight()); 390 391 double latitude = PicasaSource.getLatitude(mMediaItem); 392 double longitude = PicasaSource.getLongitude(mMediaItem); 393 if (GalleryUtils.isValidLocation(latitude, longitude)) { 394 values.put(Images.Media.LATITUDE, latitude); 395 values.put(Images.Media.LONGITUDE, longitude); 396 } 397 return getContentResolver().insert( 398 Images.Media.EXTERNAL_CONTENT_URI, values); 399 } 400 401 private Uri saveLocalImage(JobContext jc, Bitmap cropped) { 402 LocalImage localImage = (LocalImage) mMediaItem; 403 404 File oldPath = new File(localImage.filePath); 405 File directory = new File(oldPath.getParent()); 406 407 String filename = oldPath.getName(); 408 int pos = filename.lastIndexOf('.'); 409 if (pos >= 0) filename = filename.substring(0, pos); 410 File output = saveMedia(jc, cropped, directory, filename); 411 if (output == null) return null; 412 413 copyExif(oldPath.getAbsolutePath(), output.getAbsolutePath(), 414 cropped.getWidth(), cropped.getHeight()); 415 416 long now = System.currentTimeMillis() / 1000; 417 ContentValues values = new ContentValues(); 418 values.put(Images.Media.TITLE, localImage.caption); 419 values.put(Images.Media.DISPLAY_NAME, output.getName()); 420 values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs); 421 values.put(Images.Media.DATE_MODIFIED, now); 422 values.put(Images.Media.DATE_ADDED, now); 423 values.put(Images.Media.MIME_TYPE, getOutputMimeType()); 424 values.put(Images.Media.ORIENTATION, 0); 425 values.put(Images.Media.DATA, output.getAbsolutePath()); 426 values.put(Images.Media.SIZE, output.length()); 427 values.put(WIDTH, cropped.getWidth()); 428 values.put(HEIGHT, cropped.getHeight()); 429 430 if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) { 431 values.put(Images.Media.LATITUDE, localImage.latitude); 432 values.put(Images.Media.LONGITUDE, localImage.longitude); 433 } 434 return getContentResolver().insert( 435 Images.Media.EXTERNAL_CONTENT_URI, values); 436 } 437 438 private Uri saveGenericImage(JobContext jc, Bitmap cropped) { 439 if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) { 440 throw new RuntimeException("cannot create download folder"); 441 } 442 443 long now = System.currentTimeMillis(); 444 String filename = new SimpleDateFormat(TIME_STAMP_NAME). 445 format(new Date(now)); 446 447 File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename); 448 if (output == null) return null; 449 450 ContentValues values = new ContentValues(); 451 values.put(Images.Media.TITLE, filename); 452 values.put(Images.Media.DISPLAY_NAME, output.getName()); 453 values.put(Images.Media.DATE_TAKEN, now); 454 values.put(Images.Media.DATE_MODIFIED, now / 1000); 455 values.put(Images.Media.DATE_ADDED, now / 1000); 456 values.put(Images.Media.MIME_TYPE, getOutputMimeType()); 457 values.put(Images.Media.ORIENTATION, 0); 458 values.put(Images.Media.DATA, output.getAbsolutePath()); 459 values.put(Images.Media.SIZE, output.length()); 460 values.put(WIDTH, cropped.getWidth()); 461 values.put(HEIGHT, cropped.getHeight()); 462 463 return getContentResolver().insert( 464 Images.Media.EXTERNAL_CONTENT_URI, values); 465 } 466 467 private boolean saveBitmapToOutputStream( 468 JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) { 469 // We wrap the OutputStream so that it can be interrupted. 470 final InterruptableOutputStream ios = new InterruptableOutputStream(os); 471 jc.setCancelListener(new CancelListener() { 472 public void onCancel() { 473 ios.interrupt(); 474 } 475 }); 476 try { 477 bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os); 478 return !jc.isCancelled(); 479 } finally { 480 jc.setCancelListener(null); 481 Utils.closeSilently(os); 482 } 483 } 484 485 private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) { 486 try { 487 return saveBitmapToOutputStream(jc, bitmap, 488 convertExtensionToCompressFormat(getFileExtension()), 489 getContentResolver().openOutputStream(uri)); 490 } catch (FileNotFoundException e) { 491 Log.w(TAG, "cannot write output", e); 492 } 493 return true; 494 } 495 496 private CompressFormat convertExtensionToCompressFormat(String extension) { 497 return extension.equals("png") 498 ? CompressFormat.PNG 499 : CompressFormat.JPEG; 500 } 501 502 private String getOutputMimeType() { 503 return getFileExtension().equals("png") ? "image/png" : "image/jpeg"; 504 } 505 506 private String getFileExtension() { 507 String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT); 508 String outputFormat = (requestFormat == null) 509 ? determineCompressFormat(mMediaItem) 510 : requestFormat; 511 512 outputFormat = outputFormat.toLowerCase(); 513 return (outputFormat.equals("png") || outputFormat.equals("gif")) 514 ? "png" // We don't support gif compression. 515 : "jpg"; 516 } 517 518 private void onSaveClicked() { 519 Bundle extra = getIntent().getExtras(); 520 RectF cropRect = mCropView.getCropRectangle(); 521 if (cropRect == null) return; 522 mState = STATE_SAVING; 523 int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER) 524 ? R.string.wallpaper 525 : R.string.saving_image; 526 mProgressDialog = ProgressDialog.show( 527 this, null, getString(messageId), true, false); 528 mSaveTask = getThreadPool().submit(new SaveOutput(cropRect), 529 new FutureListener<Intent>() { 530 public void onFutureDone(Future<Intent> future) { 531 mSaveTask = null; 532 if (future.isCancelled()) return; 533 Intent intent = future.get(); 534 if (intent != null) { 535 mMainHandler.sendMessage(mMainHandler.obtainMessage( 536 MSG_SAVE_COMPLETE, intent)); 537 } else { 538 mMainHandler.sendEmptyMessage(MSG_SHOW_SAVE_ERROR); 539 } 540 } 541 }); 542 } 543 544 private Bitmap getCroppedImage(Rect rect) { 545 Utils.assertTrue(rect.width() > 0 && rect.height() > 0); 546 547 Bundle extras = getIntent().getExtras(); 548 // (outputX, outputY) = the width and height of the returning bitmap. 549 int outputX = rect.width(); 550 int outputY = rect.height(); 551 if (extras != null) { 552 outputX = extras.getInt(KEY_OUTPUT_X, outputX); 553 outputY = extras.getInt(KEY_OUTPUT_Y, outputY); 554 } 555 556 if (outputX * outputY > MAX_PIXEL_COUNT) { 557 float scale = (float) Math.sqrt( 558 (double) MAX_PIXEL_COUNT / outputX / outputY); 559 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 596 if (mUseRegionDecoder) { 597 int rotation = mMediaItem.getFullImageRotation(); 598 rotateRectangle(rect, mCropView.getImageWidth(), 599 mCropView.getImageHeight(), 360 - rotation); 600 rotateRectangle(dest, outputX, outputY, 360 - rotation); 601 602 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 { 621 int rotation = mMediaItem.getRotation(); 622 rotateRectangle(rect, mCropView.getImageWidth(), 623 mCropView.getImageHeight(), 360 - rotation); 624 rotateRectangle(dest, outputX, outputY, 360 - rotation); 625 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) { 711 Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show(); 712 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( 727 mBitmap, regionDecoder), mMediaItem.getFullImageRotation()); 728 if (mDoFaceDetection) { 729 mCropView.detectFaces(mBitmap); 730 } else { 731 mCropView.initializeHighlightRectangle(); 732 } 733 } 734 735 private void onBitmapAvailable(Bitmap bitmap) { 736 if (bitmap == null) { 737 Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show(); 738 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 } 926 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 } 1012 } 1013