1 /* 2 * Copyright (C) 2011 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.ex.photo.util; 19 20 import android.content.ContentResolver; 21 import android.graphics.Bitmap; 22 import android.graphics.BitmapFactory; 23 import android.graphics.Matrix; 24 import android.graphics.Point; 25 import android.graphics.Rect; 26 import android.net.Uri; 27 import android.os.Build; 28 import android.util.Base64; 29 import android.util.Log; 30 31 import com.android.ex.photo.PhotoViewActivity; 32 import com.android.ex.photo.loaders.PhotoBitmapLoader.BitmapResult; 33 34 import java.io.ByteArrayInputStream; 35 import java.io.ByteArrayOutputStream; 36 import java.io.FileNotFoundException; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.net.MalformedURLException; 40 import java.net.URL; 41 import java.util.regex.Pattern; 42 43 44 /** 45 * Image utilities 46 */ 47 public class ImageUtils { 48 // Logging 49 private static final String TAG = "ImageUtils"; 50 51 /** Minimum class memory class to use full-res photos */ 52 private final static long MIN_NORMAL_CLASS = 32; 53 /** Minimum class memory class to use small photos */ 54 private final static long MIN_SMALL_CLASS = 24; 55 56 private static final String BASE64_URI_PREFIX = "base64,"; 57 private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*"); 58 59 public static enum ImageSize { 60 EXTRA_SMALL, 61 SMALL, 62 NORMAL, 63 } 64 65 public static final ImageSize sUseImageSize; 66 static { 67 // On HC and beyond, assume devices are more capable 68 if (Build.VERSION.SDK_INT >= 11) { 69 sUseImageSize = ImageSize.NORMAL; 70 } else { 71 if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) { 72 // We have plenty of memory; use full sized photos 73 sUseImageSize = ImageSize.NORMAL; 74 } else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) { 75 // We have slight less memory; use smaller sized photos 76 sUseImageSize = ImageSize.SMALL; 77 } else { 78 // We have little memory; use very small sized photos 79 sUseImageSize = ImageSize.EXTRA_SMALL; 80 } 81 } 82 } 83 84 /** 85 * @return true if the MimeType type is image 86 */ 87 public static boolean isImageMimeType(String mimeType) { 88 return mimeType != null && mimeType.startsWith("image/"); 89 } 90 91 /** 92 * Create a bitmap from a local URI 93 * 94 * @param resolver The ContentResolver 95 * @param uri The local URI 96 * @param maxSize The maximum size (either width or height) 97 * 98 * @return The new bitmap or null 99 */ 100 public static BitmapResult createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) { 101 // TODO: make this method not download the image for both getImageBounds and decodeStream 102 BitmapResult result = new BitmapResult(); 103 InputStream inputStream = null; 104 try { 105 final BitmapFactory.Options opts = new BitmapFactory.Options(); 106 final Point bounds = getImageBounds(resolver, uri); 107 inputStream = openInputStream(resolver, uri); 108 if (bounds == null || inputStream == null) { 109 result.status = BitmapResult.STATUS_EXCEPTION; 110 return result; 111 } 112 opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize); 113 114 final Bitmap decodedBitmap = decodeStream(inputStream, null, opts); 115 116 // Correct thumbnail orientation as necessary 117 // TODO: Fix rotation if it's actually a problem 118 //return rotateBitmap(resolver, uri, decodedBitmap); 119 result.bitmap = decodedBitmap; 120 result.status = BitmapResult.STATUS_SUCCESS; 121 return result; 122 123 } catch (FileNotFoundException exception) { 124 // Do nothing - the photo will appear to be missing 125 } catch (IOException exception) { 126 result.status = BitmapResult.STATUS_EXCEPTION; 127 } catch (IllegalArgumentException exception) { 128 // Do nothing - the photo will appear to be missing 129 } catch (SecurityException exception) { 130 result.status = BitmapResult.STATUS_EXCEPTION; 131 } finally { 132 try { 133 if (inputStream != null) { 134 inputStream.close(); 135 } 136 } catch (IOException ignore) { 137 } 138 } 139 return result; 140 } 141 142 /** 143 * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect, 144 * BitmapFactory.Options)} that returns {@code null} on {@link 145 * OutOfMemoryError}. 146 * 147 * @param is The input stream that holds the raw data to be decoded into a 148 * bitmap. 149 * @param outPadding If not null, return the padding rect for the bitmap if 150 * it exists, otherwise set padding to [-1,-1,-1,-1]. If 151 * no bitmap is returned (null) then padding is 152 * unchanged. 153 * @param opts null-ok; Options that control downsampling and whether the 154 * image should be completely decoded, or just is size returned. 155 * @return The decoded bitmap, or null if the image data could not be 156 * decoded, or, if opts is non-null, if opts requested only the 157 * size be returned (in opts.outWidth and opts.outHeight) 158 */ 159 public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) { 160 ByteArrayOutputStream out = null; 161 InputStream byteStream = null; 162 try { 163 out = new ByteArrayOutputStream(); 164 final byte[] buffer = new byte[4096]; 165 int n = is.read(buffer); 166 while (n >= 0) { 167 out.write(buffer, 0, n); 168 n = is.read(buffer); 169 } 170 171 final byte[] bitmapBytes = out.toByteArray(); 172 173 // Determine the orientation for this image 174 final int orientation = Exif.getOrientation(bitmapBytes); 175 176 // Create an InputStream from this byte array 177 byteStream = new ByteArrayInputStream(bitmapBytes); 178 179 final Bitmap originalBitmap = BitmapFactory.decodeStream(byteStream, outPadding, opts); 180 181 if (byteStream != null && originalBitmap == null && !opts.inJustDecodeBounds) { 182 Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): " 183 + "Image bytes cannot be decoded into a Bitmap"); 184 throw new UnsupportedOperationException( 185 "Image bytes cannot be decoded into a Bitmap."); 186 } 187 if (originalBitmap != null && orientation != 0) { 188 final Matrix matrix = new Matrix(); 189 matrix.postRotate(orientation); 190 return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), 191 originalBitmap.getHeight(), matrix, true); 192 } 193 return originalBitmap; 194 } catch (OutOfMemoryError oome) { 195 Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome); 196 return null; 197 } catch (IOException ioe) { 198 Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe); 199 return null; 200 } finally { 201 if (out != null) { 202 try { 203 out.close(); 204 } catch (IOException e) { 205 // Do nothing 206 } 207 } 208 if (byteStream != null) { 209 try { 210 byteStream.close(); 211 } catch (IOException e) { 212 // Do nothing 213 } 214 } 215 } 216 } 217 218 /** 219 * Gets the image bounds 220 * 221 * @param resolver The ContentResolver 222 * @param uri The uri 223 * 224 * @return The image bounds 225 */ 226 private static Point getImageBounds(ContentResolver resolver, Uri uri) 227 throws IOException { 228 final BitmapFactory.Options opts = new BitmapFactory.Options(); 229 InputStream inputStream = null; 230 String scheme = uri.getScheme(); 231 try { 232 opts.inJustDecodeBounds = true; 233 inputStream = openInputStream(resolver, uri); 234 if (inputStream == null) { 235 return null; 236 } 237 decodeStream(inputStream, null, opts); 238 239 return new Point(opts.outWidth, opts.outHeight); 240 } finally { 241 try { 242 if (inputStream != null) { 243 inputStream.close(); 244 } 245 } catch (IOException ignore) { 246 } 247 } 248 } 249 250 private static InputStream openInputStream(ContentResolver resolver, Uri uri) throws 251 FileNotFoundException { 252 String scheme = uri.getScheme(); 253 if ("http".equals(scheme) || "https".equals(scheme)) { 254 try { 255 return new URL(uri.toString()).openStream(); 256 } catch (MalformedURLException e) { 257 // Fall-back to the previous behaviour, just in case 258 Log.w(TAG, "Could not convert the uri to url: " + uri.toString()); 259 return resolver.openInputStream(uri); 260 } catch (IOException e) { 261 Log.w(TAG, "Could not open input stream for uri: " + uri.toString()); 262 return null; 263 } 264 } else if ("data".equals(scheme)) { 265 byte[] data = parseDataUri(uri); 266 if (data != null) { 267 return new ByteArrayInputStream(data); 268 } 269 } 270 return resolver.openInputStream(uri); 271 } 272 273 private static byte[] parseDataUri(Uri uri) { 274 String ssp = uri.getSchemeSpecificPart(); 275 try { 276 if (ssp.startsWith(BASE64_URI_PREFIX)) { 277 String base64 = ssp.substring(BASE64_URI_PREFIX.length()); 278 return Base64.decode(base64, Base64.URL_SAFE); 279 } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){ 280 String base64 = ssp.substring( 281 ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length()); 282 return Base64.decode(base64, Base64.DEFAULT); 283 } else { 284 return null; 285 } 286 } catch (IllegalArgumentException ex) { 287 Log.e(TAG, "Mailformed data URI: " + ex); 288 return null; 289 } 290 } 291 } 292