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.PhotoBitmapLoaderInterface.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 * @return The new bitmap or null 98 */ 99 public static BitmapResult createLocalBitmap(final ContentResolver resolver, final Uri uri, 100 final int maxSize) { 101 final BitmapResult result = new BitmapResult(); 102 final InputStreamFactory factory = createInputStreamFactory(resolver, uri); 103 try { 104 final Point bounds = getImageBounds(factory); 105 if (bounds == null) { 106 result.status = BitmapResult.STATUS_EXCEPTION; 107 return result; 108 } 109 110 final BitmapFactory.Options opts = new BitmapFactory.Options(); 111 opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize); 112 result.bitmap = decodeStream(factory, null, opts); 113 result.status = BitmapResult.STATUS_SUCCESS; 114 return result; 115 116 } catch (FileNotFoundException exception) { 117 // Do nothing - the photo will appear to be missing 118 } catch (IOException exception) { 119 result.status = BitmapResult.STATUS_EXCEPTION; 120 } catch (IllegalArgumentException exception) { 121 // Do nothing - the photo will appear to be missing 122 } catch (SecurityException exception) { 123 result.status = BitmapResult.STATUS_EXCEPTION; 124 } 125 return result; 126 } 127 128 /** 129 * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect, 130 * BitmapFactory.Options)} that returns {@code null} on {@link 131 * OutOfMemoryError}. 132 * 133 * @param factory Used to create input streams that holds the raw data to be decoded into a 134 * bitmap. 135 * @param outPadding If not null, return the padding rect for the bitmap if 136 * it exists, otherwise set padding to [-1,-1,-1,-1]. If 137 * no bitmap is returned (null) then padding is 138 * unchanged. 139 * @param opts null-ok; Options that control downsampling and whether the 140 * image should be completely decoded, or just is size returned. 141 * @return The decoded bitmap, or null if the image data could not be 142 * decoded, or, if opts is non-null, if opts requested only the 143 * size be returned (in opts.outWidth and opts.outHeight) 144 */ 145 public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding, 146 final BitmapFactory.Options opts) throws FileNotFoundException { 147 InputStream is = null; 148 try { 149 // Determine the orientation for this image 150 is = factory.createInputStream(); 151 final int orientation = Exif.getOrientation(is, -1); 152 is.close(); 153 154 // Decode the bitmap 155 is = factory.createInputStream(); 156 final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts); 157 158 if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) { 159 Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): " 160 + "Image bytes cannot be decoded into a Bitmap"); 161 throw new UnsupportedOperationException( 162 "Image bytes cannot be decoded into a Bitmap."); 163 } 164 165 // Rotate the Bitmap based on the orientation 166 if (originalBitmap != null && orientation != 0) { 167 final Matrix matrix = new Matrix(); 168 matrix.postRotate(orientation); 169 return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), 170 originalBitmap.getHeight(), matrix, true); 171 } 172 return originalBitmap; 173 } catch (OutOfMemoryError oome) { 174 Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome); 175 return null; 176 } catch (IOException ioe) { 177 Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe); 178 return null; 179 } finally { 180 if (is != null) { 181 try { 182 is.close(); 183 } catch (IOException e) { 184 // Do nothing 185 } 186 } 187 } 188 } 189 190 /** 191 * Gets the image bounds 192 * 193 * @param factory Used to create the InputStream. 194 * 195 * @return The image bounds 196 */ 197 private static Point getImageBounds(final InputStreamFactory factory) 198 throws IOException { 199 final BitmapFactory.Options opts = new BitmapFactory.Options(); 200 opts.inJustDecodeBounds = true; 201 decodeStream(factory, null, opts); 202 203 return new Point(opts.outWidth, opts.outHeight); 204 } 205 206 private static InputStreamFactory createInputStreamFactory(final ContentResolver resolver, 207 final Uri uri) { 208 final String scheme = uri.getScheme(); 209 if ("http".equals(scheme) || "https".equals(scheme)) { 210 return new HttpInputStreamFactory(resolver, uri); 211 } else if ("data".equals(scheme)) { 212 return new DataInputStreamFactory(resolver, uri); 213 } 214 return new BaseInputStreamFactory(resolver, uri); 215 } 216 217 /** 218 * Utility class for when an InputStream needs to be read multiple times. For example, one pass 219 * may load EXIF orientation, and the second pass may do the actual Bitmap decode. 220 */ 221 public interface InputStreamFactory { 222 223 /** 224 * Create a new InputStream. The caller of this method must be able to read the input 225 * stream starting from the beginning. 226 * @return 227 */ 228 InputStream createInputStream() throws FileNotFoundException; 229 } 230 231 private static class BaseInputStreamFactory implements InputStreamFactory { 232 protected final ContentResolver mResolver; 233 protected final Uri mUri; 234 235 public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) { 236 mResolver = resolver; 237 mUri = uri; 238 } 239 240 @Override 241 public InputStream createInputStream() throws FileNotFoundException { 242 return mResolver.openInputStream(mUri); 243 } 244 } 245 246 private static class DataInputStreamFactory extends BaseInputStreamFactory { 247 private byte[] mData; 248 249 public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) { 250 super(resolver, uri); 251 } 252 253 @Override 254 public InputStream createInputStream() throws FileNotFoundException { 255 if (mData == null) { 256 mData = parseDataUri(mUri); 257 if (mData == null) { 258 return super.createInputStream(); 259 } 260 } 261 return new ByteArrayInputStream(mData); 262 } 263 264 private byte[] parseDataUri(final Uri uri) { 265 final String ssp = uri.getSchemeSpecificPart(); 266 try { 267 if (ssp.startsWith(BASE64_URI_PREFIX)) { 268 final String base64 = ssp.substring(BASE64_URI_PREFIX.length()); 269 return Base64.decode(base64, Base64.URL_SAFE); 270 } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){ 271 final String base64 = ssp.substring( 272 ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length()); 273 return Base64.decode(base64, Base64.DEFAULT); 274 } else { 275 return null; 276 } 277 } catch (IllegalArgumentException ex) { 278 Log.e(TAG, "Mailformed data URI: " + ex); 279 return null; 280 } 281 } 282 } 283 284 private static class HttpInputStreamFactory extends BaseInputStreamFactory { 285 private byte[] mData; 286 287 public HttpInputStreamFactory(final ContentResolver resolver, final Uri uri) { 288 super(resolver, uri); 289 } 290 291 @Override 292 public InputStream createInputStream() throws FileNotFoundException { 293 if (mData == null) { 294 mData = downloadBytes(); 295 if (mData == null) { 296 return super.createInputStream(); 297 } 298 } 299 return new ByteArrayInputStream(mData); 300 } 301 302 private byte[] downloadBytes() throws FileNotFoundException { 303 InputStream is = null; 304 ByteArrayOutputStream out = null; 305 try { 306 try { 307 is = new URL(mUri.toString()).openStream(); 308 } catch (MalformedURLException e) { 309 return null; 310 } 311 out = new ByteArrayOutputStream(); 312 final byte[] buffer = new byte[4096]; 313 int n = is.read(buffer); 314 while (n >= 0) { 315 out.write(buffer, 0, n); 316 n = is.read(buffer); 317 } 318 319 return out.toByteArray(); 320 } catch (IOException ignored) { 321 } finally { 322 if (is != null) { 323 try { 324 is.close(); 325 } catch (IOException ignored) { 326 } 327 } 328 if (out != null) { 329 try { 330 out.close(); 331 } catch (IOException ignored) { 332 } 333 } 334 } 335 return null; 336 } 337 } 338 } 339