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.PhotoViewController; 32 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult; 33 34 import java.io.ByteArrayInputStream; 35 import java.io.FileNotFoundException; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.util.regex.Pattern; 39 40 41 /** 42 * Image utilities 43 */ 44 public class ImageUtils { 45 // Logging 46 private static final String TAG = "ImageUtils"; 47 48 /** Minimum class memory class to use full-res photos */ 49 private final static long MIN_NORMAL_CLASS = 32; 50 /** Minimum class memory class to use small photos */ 51 private final static long MIN_SMALL_CLASS = 24; 52 53 private static final String BASE64_URI_PREFIX = "base64,"; 54 private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*"); 55 56 public static enum ImageSize { 57 EXTRA_SMALL, 58 SMALL, 59 NORMAL, 60 } 61 62 public static final ImageSize sUseImageSize; 63 static { 64 // On HC and beyond, assume devices are more capable 65 if (Build.VERSION.SDK_INT >= 11) { 66 sUseImageSize = ImageSize.NORMAL; 67 } else { 68 if (PhotoViewController.sMemoryClass >= MIN_NORMAL_CLASS) { 69 // We have plenty of memory; use full sized photos 70 sUseImageSize = ImageSize.NORMAL; 71 } else if (PhotoViewController.sMemoryClass >= MIN_SMALL_CLASS) { 72 // We have slight less memory; use smaller sized photos 73 sUseImageSize = ImageSize.SMALL; 74 } else { 75 // We have little memory; use very small sized photos 76 sUseImageSize = ImageSize.EXTRA_SMALL; 77 } 78 } 79 } 80 81 /** 82 * @return true if the MimeType type is image 83 */ isImageMimeType(String mimeType)84 public static boolean isImageMimeType(String mimeType) { 85 return mimeType != null && mimeType.startsWith("image/"); 86 } 87 88 /** 89 * Create a bitmap from a local URI 90 * 91 * @param resolver The ContentResolver 92 * @param uri The local URI 93 * @param maxSize The maximum size (either width or height) 94 * @return The new bitmap or null 95 */ createLocalBitmap(final ContentResolver resolver, final Uri uri, final int maxSize)96 public static BitmapResult createLocalBitmap(final ContentResolver resolver, final Uri uri, 97 final int maxSize) { 98 final BitmapResult result = new BitmapResult(); 99 final InputStreamFactory factory = createInputStreamFactory(resolver, uri); 100 try { 101 final Point bounds = getImageBounds(factory); 102 if (bounds == null) { 103 result.status = BitmapResult.STATUS_EXCEPTION; 104 return result; 105 } 106 107 final BitmapFactory.Options opts = new BitmapFactory.Options(); 108 opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize); 109 result.bitmap = decodeStream(factory, null, opts); 110 result.status = BitmapResult.STATUS_SUCCESS; 111 return result; 112 113 } catch (FileNotFoundException exception) { 114 // Do nothing - the photo will appear to be missing 115 } catch (IOException exception) { 116 result.status = BitmapResult.STATUS_EXCEPTION; 117 } catch (IllegalArgumentException exception) { 118 // Do nothing - the photo will appear to be missing 119 } catch (SecurityException exception) { 120 result.status = BitmapResult.STATUS_EXCEPTION; 121 } 122 return result; 123 } 124 125 /** 126 * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect, 127 * BitmapFactory.Options)} that returns {@code null} on {@link 128 * OutOfMemoryError}. 129 * 130 * @param factory Used to create input streams that holds the raw data to be decoded into a 131 * bitmap. 132 * @param outPadding If not null, return the padding rect for the bitmap if 133 * it exists, otherwise set padding to [-1,-1,-1,-1]. If 134 * no bitmap is returned (null) then padding is 135 * unchanged. 136 * @param opts null-ok; Options that control downsampling and whether the 137 * image should be completely decoded, or just is size returned. 138 * @return The decoded bitmap, or null if the image data could not be 139 * decoded, or, if opts is non-null, if opts requested only the 140 * size be returned (in opts.outWidth and opts.outHeight) 141 */ decodeStream(final InputStreamFactory factory, final Rect outPadding, final BitmapFactory.Options opts)142 public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding, 143 final BitmapFactory.Options opts) throws FileNotFoundException { 144 InputStream is = null; 145 try { 146 // Determine the orientation for this image 147 is = factory.createInputStream(); 148 final int orientation = Exif.getOrientation(is, -1); 149 if (is != null) { 150 is.close(); 151 } 152 153 // Decode the bitmap 154 is = factory.createInputStream(); 155 final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts); 156 157 if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) { 158 Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): " 159 + "Image bytes cannot be decoded into a Bitmap"); 160 throw new UnsupportedOperationException( 161 "Image bytes cannot be decoded into a Bitmap."); 162 } 163 164 // Rotate the Bitmap based on the orientation 165 if (originalBitmap != null && orientation != 0) { 166 final Matrix matrix = new Matrix(); 167 matrix.postRotate(orientation); 168 return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), 169 originalBitmap.getHeight(), matrix, true); 170 } 171 return originalBitmap; 172 } catch (OutOfMemoryError oome) { 173 Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome); 174 return null; 175 } catch (IOException ioe) { 176 Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe); 177 return null; 178 } finally { 179 if (is != null) { 180 try { 181 is.close(); 182 } catch (IOException e) { 183 // Do nothing 184 } 185 } 186 } 187 } 188 189 /** 190 * Gets the image bounds 191 * 192 * @param factory Used to create the InputStream. 193 * 194 * @return The image bounds 195 */ getImageBounds(final InputStreamFactory factory)196 private static Point getImageBounds(final InputStreamFactory factory) 197 throws IOException { 198 final BitmapFactory.Options opts = new BitmapFactory.Options(); 199 opts.inJustDecodeBounds = true; 200 decodeStream(factory, null, opts); 201 202 return new Point(opts.outWidth, opts.outHeight); 203 } 204 createInputStreamFactory(final ContentResolver resolver, final Uri uri)205 private static InputStreamFactory createInputStreamFactory(final ContentResolver resolver, 206 final Uri uri) { 207 final String scheme = uri.getScheme(); 208 if ("data".equals(scheme)) { 209 return new DataInputStreamFactory(resolver, uri); 210 } 211 return new BaseInputStreamFactory(resolver, uri); 212 } 213 214 /** 215 * Utility class for when an InputStream needs to be read multiple times. For example, one pass 216 * may load EXIF orientation, and the second pass may do the actual Bitmap decode. 217 */ 218 public interface InputStreamFactory { 219 220 /** 221 * Create a new InputStream. The caller of this method must be able to read the input 222 * stream starting from the beginning. 223 * @return 224 */ createInputStream()225 InputStream createInputStream() throws FileNotFoundException; 226 } 227 228 private static class BaseInputStreamFactory implements InputStreamFactory { 229 protected final ContentResolver mResolver; 230 protected final Uri mUri; 231 BaseInputStreamFactory(final ContentResolver resolver, final Uri uri)232 public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) { 233 mResolver = resolver; 234 mUri = uri; 235 } 236 237 @Override createInputStream()238 public InputStream createInputStream() throws FileNotFoundException { 239 return mResolver.openInputStream(mUri); 240 } 241 } 242 243 private static class DataInputStreamFactory extends BaseInputStreamFactory { 244 private byte[] mData; 245 DataInputStreamFactory(final ContentResolver resolver, final Uri uri)246 public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) { 247 super(resolver, uri); 248 } 249 250 @Override createInputStream()251 public InputStream createInputStream() throws FileNotFoundException { 252 if (mData == null) { 253 mData = parseDataUri(mUri); 254 if (mData == null) { 255 return super.createInputStream(); 256 } 257 } 258 return new ByteArrayInputStream(mData); 259 } 260 parseDataUri(final Uri uri)261 private byte[] parseDataUri(final Uri uri) { 262 final String ssp = uri.getSchemeSpecificPart(); 263 try { 264 if (ssp.startsWith(BASE64_URI_PREFIX)) { 265 final String base64 = ssp.substring(BASE64_URI_PREFIX.length()); 266 return Base64.decode(base64, Base64.URL_SAFE); 267 } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){ 268 final String base64 = ssp.substring( 269 ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length()); 270 return Base64.decode(base64, Base64.DEFAULT); 271 } else { 272 return null; 273 } 274 } catch (IllegalArgumentException ex) { 275 Log.e(TAG, "Mailformed data URI: " + ex); 276 return null; 277 } 278 } 279 } 280 } 281