1 /* 2 * Copyright (C) 2012 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.camera.crop; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.database.Cursor; 23 import android.database.sqlite.SQLiteException; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.graphics.Matrix; 27 import android.graphics.Rect; 28 import android.net.Uri; 29 import android.provider.MediaStore; 30 import android.util.Log; 31 import android.webkit.MimeTypeMap; 32 33 import com.android.camera.exif.ExifInterface; 34 import com.android.camera.exif.ExifTag; 35 36 import java.io.FileNotFoundException; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.util.List; 40 41 public final class ImageLoader { 42 43 private static final String LOGTAG = "ImageLoader"; 44 45 public static final String JPEG_MIME_TYPE = "image/jpeg"; 46 public static final int DEFAULT_COMPRESS_QUALITY = 95; 47 48 public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT; 49 public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP; 50 public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT; 51 public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM; 52 public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT; 53 public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT; 54 public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP; 55 public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM; 56 57 private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5; 58 private static final float OVERDRAW_ZOOM = 1.2f; ImageLoader()59 private ImageLoader() {} 60 61 /** 62 * Returns the Mime type for a Url. Safe to use with Urls that do not 63 * come from Gallery's content provider. 64 */ getMimeType(Uri src)65 public static String getMimeType(Uri src) { 66 String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString()); 67 String ret = null; 68 if (postfix != null) { 69 ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix); 70 } 71 return ret; 72 } 73 getLocalPathFromUri(Context context, Uri uri)74 public static String getLocalPathFromUri(Context context, Uri uri) { 75 Cursor cursor = context.getContentResolver().query(uri, 76 new String[]{MediaStore.Images.Media.DATA}, null, null, null); 77 if (cursor == null) { 78 return null; 79 } 80 int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); 81 cursor.moveToFirst(); 82 return cursor.getString(index); 83 } 84 85 /** 86 * Returns the image's orientation flag. Defaults to ORI_NORMAL if no valid 87 * orientation was found. 88 */ getMetadataOrientation(Context context, Uri uri)89 public static int getMetadataOrientation(Context context, Uri uri) { 90 if (uri == null || context == null) { 91 throw new IllegalArgumentException("bad argument to getOrientation"); 92 } 93 94 // First try to find orientation data in Gallery's ContentProvider. 95 Cursor cursor = null; 96 try { 97 cursor = context.getContentResolver().query(uri, 98 new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, 99 null, null, null); 100 if (cursor != null && cursor.moveToNext()) { 101 int ori = cursor.getInt(0); 102 switch (ori) { 103 case 90: 104 return ORI_ROTATE_90; 105 case 270: 106 return ORI_ROTATE_270; 107 case 180: 108 return ORI_ROTATE_180; 109 default: 110 return ORI_NORMAL; 111 } 112 } 113 } catch (SQLiteException e) { 114 // Do nothing 115 } catch (IllegalArgumentException e) { 116 // Do nothing 117 } catch (IllegalStateException e) { 118 // Do nothing 119 } finally { 120 Utils.closeSilently(cursor); 121 } 122 123 // Fall back to checking EXIF tags in file. 124 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 125 String mimeType = getMimeType(uri); 126 if (!JPEG_MIME_TYPE.equals(mimeType)) { 127 return ORI_NORMAL; 128 } 129 String path = uri.getPath(); 130 ExifInterface exif = new ExifInterface(); 131 try { 132 exif.readExif(path); 133 Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); 134 if (tagval != null) { 135 int orientation = tagval; 136 switch(orientation) { 137 case ORI_NORMAL: 138 case ORI_ROTATE_90: 139 case ORI_ROTATE_180: 140 case ORI_ROTATE_270: 141 case ORI_FLIP_HOR: 142 case ORI_FLIP_VERT: 143 case ORI_TRANSPOSE: 144 case ORI_TRANSVERSE: 145 return orientation; 146 default: 147 return ORI_NORMAL; 148 } 149 } 150 } catch (IOException e) { 151 Log.w(LOGTAG, "Failed to read EXIF orientation", e); 152 } 153 } 154 return ORI_NORMAL; 155 } 156 157 /** 158 * Returns the rotation of image at the given URI as one of 0, 90, 180, 159 * 270. Defaults to 0. 160 */ getMetadataRotation(Context context, Uri uri)161 public static int getMetadataRotation(Context context, Uri uri) { 162 int orientation = getMetadataOrientation(context, uri); 163 switch(orientation) { 164 case ORI_ROTATE_90: 165 return 90; 166 case ORI_ROTATE_180: 167 return 180; 168 case ORI_ROTATE_270: 169 return 270; 170 default: 171 return 0; 172 } 173 } 174 175 /** 176 * Takes an orientation and a bitmap, and returns the bitmap transformed 177 * to that orientation. 178 */ orientBitmap(Bitmap bitmap, int ori)179 public static Bitmap orientBitmap(Bitmap bitmap, int ori) { 180 Matrix matrix = new Matrix(); 181 int w = bitmap.getWidth(); 182 int h = bitmap.getHeight(); 183 if (ori == ORI_ROTATE_90 || 184 ori == ORI_ROTATE_270 || 185 ori == ORI_TRANSPOSE || 186 ori == ORI_TRANSVERSE) { 187 int tmp = w; 188 w = h; 189 h = tmp; 190 } 191 switch (ori) { 192 case ORI_ROTATE_90: 193 matrix.setRotate(90, w / 2f, h / 2f); 194 break; 195 case ORI_ROTATE_180: 196 matrix.setRotate(180, w / 2f, h / 2f); 197 break; 198 case ORI_ROTATE_270: 199 matrix.setRotate(270, w / 2f, h / 2f); 200 break; 201 case ORI_FLIP_HOR: 202 matrix.preScale(-1, 1); 203 break; 204 case ORI_FLIP_VERT: 205 matrix.preScale(1, -1); 206 break; 207 case ORI_TRANSPOSE: 208 matrix.setRotate(90, w / 2f, h / 2f); 209 matrix.preScale(1, -1); 210 break; 211 case ORI_TRANSVERSE: 212 matrix.setRotate(270, w / 2f, h / 2f); 213 matrix.preScale(1, -1); 214 break; 215 case ORI_NORMAL: 216 default: 217 return bitmap; 218 } 219 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), 220 bitmap.getHeight(), matrix, true); 221 } 222 223 /** 224 * Returns the bounds of the bitmap stored at a given Url. 225 */ loadBitmapBounds(Context context, Uri uri)226 public static Rect loadBitmapBounds(Context context, Uri uri) { 227 BitmapFactory.Options o = new BitmapFactory.Options(); 228 loadBitmap(context, uri, o); 229 return new Rect(0, 0, o.outWidth, o.outHeight); 230 } 231 232 /** 233 * Loads a bitmap that has been downsampled using sampleSize from a given url. 234 */ loadDownsampledBitmap(Context context, Uri uri, int sampleSize)235 public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) { 236 BitmapFactory.Options options = new BitmapFactory.Options(); 237 options.inMutable = true; 238 options.inSampleSize = sampleSize; 239 return loadBitmap(context, uri, options); 240 } 241 242 /** 243 * Returns the bitmap from the given uri loaded using the given options. 244 * Returns null on failure. 245 */ loadBitmap(Context context, Uri uri, BitmapFactory.Options o)246 public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) { 247 if (uri == null || context == null) { 248 throw new IllegalArgumentException("bad argument to loadBitmap"); 249 } 250 InputStream is = null; 251 try { 252 is = context.getContentResolver().openInputStream(uri); 253 return BitmapFactory.decodeStream(is, null, o); 254 } catch (FileNotFoundException e) { 255 Log.e(LOGTAG, "FileNotFoundException for " + uri, e); 256 } finally { 257 Utils.closeSilently(is); 258 } 259 return null; 260 } 261 262 /** 263 * Loads a bitmap at a given URI that is downsampled so that both sides are 264 * smaller than maxSideLength. The Bitmap's original dimensions are stored 265 * in the rect originalBounds. 266 * 267 * @param uri URI of image to open. 268 * @param context context whose ContentResolver to use. 269 * @param maxSideLength max side length of returned bitmap. 270 * @param originalBounds If not null, set to the actual bounds of the stored bitmap. 271 * @param useMin use min or max side of the original image 272 * @return downsampled bitmap or null if this operation failed. 273 */ loadConstrainedBitmap(Uri uri, Context context, int maxSideLength, Rect originalBounds, boolean useMin)274 public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength, 275 Rect originalBounds, boolean useMin) { 276 if (maxSideLength <= 0 || uri == null || context == null) { 277 throw new IllegalArgumentException("bad argument to getScaledBitmap"); 278 } 279 // Get width and height of stored bitmap 280 Rect storedBounds = loadBitmapBounds(context, uri); 281 if (originalBounds != null) { 282 originalBounds.set(storedBounds); 283 } 284 int w = storedBounds.width(); 285 int h = storedBounds.height(); 286 287 // If bitmap cannot be decoded, return null 288 if (w <= 0 || h <= 0) { 289 return null; 290 } 291 292 // Find best downsampling size 293 int imageSide = 0; 294 if (useMin) { 295 imageSide = Math.min(w, h); 296 } else { 297 imageSide = Math.max(w, h); 298 } 299 int sampleSize = 1; 300 while (imageSide > maxSideLength) { 301 imageSide >>>= 1; 302 sampleSize <<= 1; 303 } 304 305 // Make sure sample size is reasonable 306 if (sampleSize <= 0 || 307 0 >= (int) (Math.min(w, h) / sampleSize)) { 308 return null; 309 } 310 return loadDownsampledBitmap(context, uri, sampleSize); 311 } 312 313 /** 314 * Loads a bitmap at a given URI that is downsampled so that both sides are 315 * smaller than maxSideLength. The Bitmap's original dimensions are stored 316 * in the rect originalBounds. The output is also transformed to the given 317 * orientation. 318 * 319 * @param uri URI of image to open. 320 * @param context context whose ContentResolver to use. 321 * @param maxSideLength max side length of returned bitmap. 322 * @param orientation the orientation to transform the bitmap to. 323 * @param originalBounds set to the actual bounds of the stored bitmap. 324 * @return downsampled bitmap or null if this operation failed. 325 */ loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength, int orientation, Rect originalBounds)326 public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength, 327 int orientation, Rect originalBounds) { 328 Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false); 329 if (bmap != null) { 330 bmap = orientBitmap(bmap, orientation); 331 if (bmap.getConfig()!= Bitmap.Config.ARGB_8888){ 332 bmap = bmap.copy( Bitmap.Config.ARGB_8888,true); 333 } 334 } 335 return bmap; 336 } 337 338 /** 339 * Loads a bitmap that is downsampled by at least the input sample size. In 340 * low-memory situations, the bitmap may be downsampled further. 341 */ loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize)342 public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) { 343 boolean noBitmap = true; 344 int num_tries = 0; 345 if (sampleSize <= 0) { 346 sampleSize = 1; 347 } 348 Bitmap bmap = null; 349 while (noBitmap) { 350 try { 351 // Try to decode, downsample if low-memory. 352 bmap = loadDownsampledBitmap(context, sourceUri, sampleSize); 353 noBitmap = false; 354 } catch (java.lang.OutOfMemoryError e) { 355 // Try with more downsampling before failing for good. 356 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { 357 throw e; 358 } 359 bmap = null; 360 System.gc(); 361 sampleSize *= 2; 362 } 363 } 364 return bmap; 365 } 366 367 /** 368 * Loads an oriented bitmap that is downsampled by at least the input sample 369 * size. In low-memory situations, the bitmap may be downsampled further. 370 */ loadOrientedBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize)371 public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri, 372 int sampleSize) { 373 Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize); 374 if (bitmap == null) { 375 return null; 376 } 377 int orientation = getMetadataOrientation(context, sourceUri); 378 bitmap = orientBitmap(bitmap, orientation); 379 return bitmap; 380 } 381 382 /** 383 * Loads bitmap from a resource that may be downsampled in low-memory situations. 384 */ decodeResourceWithBackouts(Resources res, BitmapFactory.Options options, int id)385 public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options, 386 int id) { 387 boolean noBitmap = true; 388 int num_tries = 0; 389 if (options.inSampleSize < 1) { 390 options.inSampleSize = 1; 391 } 392 // Stopgap fix for low-memory devices. 393 Bitmap bmap = null; 394 while (noBitmap) { 395 try { 396 // Try to decode, downsample if low-memory. 397 bmap = BitmapFactory.decodeResource( 398 res, id, options); 399 noBitmap = false; 400 } catch (java.lang.OutOfMemoryError e) { 401 // Retry before failing for good. 402 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { 403 throw e; 404 } 405 bmap = null; 406 System.gc(); 407 options.inSampleSize *= 2; 408 } 409 } 410 return bmap; 411 } 412 getExif(Context context, Uri uri)413 public static List<ExifTag> getExif(Context context, Uri uri) { 414 String path = getLocalPathFromUri(context, uri); 415 if (path != null) { 416 Uri localUri = Uri.parse(path); 417 String mimeType = getMimeType(localUri); 418 if (!JPEG_MIME_TYPE.equals(mimeType)) { 419 return null; 420 } 421 try { 422 ExifInterface exif = new ExifInterface(); 423 exif.readExif(path); 424 List<ExifTag> taglist = exif.getAllTags(); 425 return taglist; 426 } catch (IOException e) { 427 Log.w(LOGTAG, "Failed to read EXIF tags", e); 428 } 429 } 430 return null; 431 } 432 } 433