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.data; 18 19 import android.annotation.TargetApi; 20 import android.graphics.Bitmap; 21 import android.graphics.Bitmap.Config; 22 import android.graphics.BitmapFactory; 23 import android.graphics.BitmapFactory.Options; 24 import android.graphics.BitmapRegionDecoder; 25 import android.os.Build; 26 import android.util.FloatMath; 27 28 import com.android.gallery3d.common.ApiHelper; 29 import com.android.gallery3d.common.BitmapUtils; 30 import com.android.gallery3d.common.Utils; 31 import com.android.photos.data.GalleryBitmapPool; 32 import com.android.gallery3d.ui.Log; 33 import com.android.gallery3d.util.ThreadPool.CancelListener; 34 import com.android.gallery3d.util.ThreadPool.JobContext; 35 36 import java.io.FileDescriptor; 37 import java.io.FileInputStream; 38 import java.io.InputStream; 39 40 public class DecodeUtils { 41 private static final String TAG = "DecodeUtils"; 42 43 private static class DecodeCanceller implements CancelListener { 44 Options mOptions; 45 46 public DecodeCanceller(Options options) { 47 mOptions = options; 48 } 49 50 @Override 51 public void onCancel() { 52 mOptions.requestCancelDecode(); 53 } 54 } 55 56 @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) 57 public static void setOptionsMutable(Options options) { 58 if (ApiHelper.HAS_OPTIONS_IN_MUTABLE) options.inMutable = true; 59 } 60 61 public static Bitmap decode(JobContext jc, FileDescriptor fd, Options options) { 62 if (options == null) options = new Options(); 63 jc.setCancelListener(new DecodeCanceller(options)); 64 setOptionsMutable(options); 65 return ensureGLCompatibleBitmap( 66 BitmapFactory.decodeFileDescriptor(fd, null, options)); 67 } 68 69 public static void decodeBounds(JobContext jc, FileDescriptor fd, 70 Options options) { 71 Utils.assertTrue(options != null); 72 options.inJustDecodeBounds = true; 73 jc.setCancelListener(new DecodeCanceller(options)); 74 BitmapFactory.decodeFileDescriptor(fd, null, options); 75 options.inJustDecodeBounds = false; 76 } 77 78 public static Bitmap decode(JobContext jc, byte[] bytes, Options options) { 79 return decode(jc, bytes, 0, bytes.length, options); 80 } 81 82 public static Bitmap decode(JobContext jc, byte[] bytes, int offset, 83 int length, Options options) { 84 if (options == null) options = new Options(); 85 jc.setCancelListener(new DecodeCanceller(options)); 86 setOptionsMutable(options); 87 return ensureGLCompatibleBitmap( 88 BitmapFactory.decodeByteArray(bytes, offset, length, options)); 89 } 90 91 public static void decodeBounds(JobContext jc, byte[] bytes, int offset, 92 int length, Options options) { 93 Utils.assertTrue(options != null); 94 options.inJustDecodeBounds = true; 95 jc.setCancelListener(new DecodeCanceller(options)); 96 BitmapFactory.decodeByteArray(bytes, offset, length, options); 97 options.inJustDecodeBounds = false; 98 } 99 100 public static Bitmap decodeThumbnail( 101 JobContext jc, String filePath, Options options, int targetSize, int type) { 102 FileInputStream fis = null; 103 try { 104 fis = new FileInputStream(filePath); 105 FileDescriptor fd = fis.getFD(); 106 return decodeThumbnail(jc, fd, options, targetSize, type); 107 } catch (Exception ex) { 108 Log.w(TAG, ex); 109 return null; 110 } finally { 111 Utils.closeSilently(fis); 112 } 113 } 114 115 public static Bitmap decodeThumbnail( 116 JobContext jc, FileDescriptor fd, Options options, int targetSize, int type) { 117 if (options == null) options = new Options(); 118 jc.setCancelListener(new DecodeCanceller(options)); 119 120 options.inJustDecodeBounds = true; 121 BitmapFactory.decodeFileDescriptor(fd, null, options); 122 if (jc.isCancelled()) return null; 123 124 int w = options.outWidth; 125 int h = options.outHeight; 126 127 if (type == MediaItem.TYPE_MICROTHUMBNAIL) { 128 // We center-crop the original image as it's micro thumbnail. In this case, 129 // we want to make sure the shorter side >= "targetSize". 130 float scale = (float) targetSize / Math.min(w, h); 131 options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); 132 133 // For an extremely wide image, e.g. 300x30000, we may got OOM when decoding 134 // it for TYPE_MICROTHUMBNAIL. So we add a max number of pixels limit here. 135 final int MAX_PIXEL_COUNT = 640000; // 400 x 1600 136 if ((w / options.inSampleSize) * (h / options.inSampleSize) > MAX_PIXEL_COUNT) { 137 options.inSampleSize = BitmapUtils.computeSampleSize( 138 FloatMath.sqrt((float) MAX_PIXEL_COUNT / (w * h))); 139 } 140 } else { 141 // For screen nail, we only want to keep the longer side >= targetSize. 142 float scale = (float) targetSize / Math.max(w, h); 143 options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); 144 } 145 146 options.inJustDecodeBounds = false; 147 setOptionsMutable(options); 148 149 Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options); 150 if (result == null) return null; 151 152 // We need to resize down if the decoder does not support inSampleSize 153 // (For example, GIF images) 154 float scale = (float) targetSize / (type == MediaItem.TYPE_MICROTHUMBNAIL 155 ? Math.min(result.getWidth(), result.getHeight()) 156 : Math.max(result.getWidth(), result.getHeight())); 157 158 if (scale <= 0.5) result = BitmapUtils.resizeBitmapByScale(result, scale, true); 159 return ensureGLCompatibleBitmap(result); 160 } 161 162 /** 163 * Decodes the bitmap from the given byte array if the image size is larger than the given 164 * requirement. 165 * 166 * Note: The returned image may be resized down. However, both width and height must be 167 * larger than the <code>targetSize</code>. 168 */ 169 public static Bitmap decodeIfBigEnough(JobContext jc, byte[] data, 170 Options options, int targetSize) { 171 if (options == null) options = new Options(); 172 jc.setCancelListener(new DecodeCanceller(options)); 173 174 options.inJustDecodeBounds = true; 175 BitmapFactory.decodeByteArray(data, 0, data.length, options); 176 if (jc.isCancelled()) return null; 177 if (options.outWidth < targetSize || options.outHeight < targetSize) { 178 return null; 179 } 180 options.inSampleSize = BitmapUtils.computeSampleSizeLarger( 181 options.outWidth, options.outHeight, targetSize); 182 options.inJustDecodeBounds = false; 183 setOptionsMutable(options); 184 185 return ensureGLCompatibleBitmap( 186 BitmapFactory.decodeByteArray(data, 0, data.length, options)); 187 } 188 189 // TODO: This function should not be called directly from 190 // DecodeUtils.requestDecode(...), since we don't have the knowledge 191 // if the bitmap will be uploaded to GL. 192 public static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { 193 if (bitmap == null || bitmap.getConfig() != null) return bitmap; 194 Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); 195 bitmap.recycle(); 196 return newBitmap; 197 } 198 199 public static BitmapRegionDecoder createBitmapRegionDecoder( 200 JobContext jc, byte[] bytes, int offset, int length, 201 boolean shareable) { 202 if (offset < 0 || length <= 0 || offset + length > bytes.length) { 203 throw new IllegalArgumentException(String.format( 204 "offset = %s, length = %s, bytes = %s", 205 offset, length, bytes.length)); 206 } 207 208 try { 209 return BitmapRegionDecoder.newInstance( 210 bytes, offset, length, shareable); 211 } catch (Throwable t) { 212 Log.w(TAG, t); 213 return null; 214 } 215 } 216 217 public static BitmapRegionDecoder createBitmapRegionDecoder( 218 JobContext jc, String filePath, boolean shareable) { 219 try { 220 return BitmapRegionDecoder.newInstance(filePath, shareable); 221 } catch (Throwable t) { 222 Log.w(TAG, t); 223 return null; 224 } 225 } 226 227 public static BitmapRegionDecoder createBitmapRegionDecoder( 228 JobContext jc, FileDescriptor fd, boolean shareable) { 229 try { 230 return BitmapRegionDecoder.newInstance(fd, shareable); 231 } catch (Throwable t) { 232 Log.w(TAG, t); 233 return null; 234 } 235 } 236 237 public static BitmapRegionDecoder createBitmapRegionDecoder( 238 JobContext jc, InputStream is, boolean shareable) { 239 try { 240 return BitmapRegionDecoder.newInstance(is, shareable); 241 } catch (Throwable t) { 242 // We often cancel the creating of bitmap region decoder, 243 // so just log one line. 244 Log.w(TAG, "requestCreateBitmapRegionDecoder: " + t); 245 return null; 246 } 247 } 248 249 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 250 public static Bitmap decodeUsingPool(JobContext jc, byte[] data, int offset, 251 int length, BitmapFactory.Options options) { 252 if (options == null) options = new BitmapFactory.Options(); 253 if (options.inSampleSize < 1) options.inSampleSize = 1; 254 options.inPreferredConfig = Bitmap.Config.ARGB_8888; 255 options.inBitmap = (options.inSampleSize == 1) 256 ? findCachedBitmap(jc, data, offset, length, options) : null; 257 try { 258 Bitmap bitmap = decode(jc, data, offset, length, options); 259 if (options.inBitmap != null && options.inBitmap != bitmap) { 260 GalleryBitmapPool.getInstance().put(options.inBitmap); 261 options.inBitmap = null; 262 } 263 return bitmap; 264 } catch (IllegalArgumentException e) { 265 if (options.inBitmap == null) throw e; 266 267 Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap"); 268 GalleryBitmapPool.getInstance().put(options.inBitmap); 269 options.inBitmap = null; 270 return decode(jc, data, offset, length, options); 271 } 272 } 273 274 // This is the same as the method above except the source data comes 275 // from a file descriptor instead of a byte array. 276 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 277 public static Bitmap decodeUsingPool(JobContext jc, 278 FileDescriptor fileDescriptor, Options options) { 279 if (options == null) options = new BitmapFactory.Options(); 280 if (options.inSampleSize < 1) options.inSampleSize = 1; 281 options.inPreferredConfig = Bitmap.Config.ARGB_8888; 282 options.inBitmap = (options.inSampleSize == 1) 283 ? findCachedBitmap(jc, fileDescriptor, options) : null; 284 try { 285 Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options); 286 if (options.inBitmap != null && options.inBitmap != bitmap) { 287 GalleryBitmapPool.getInstance().put(options.inBitmap); 288 options.inBitmap = null; 289 } 290 return bitmap; 291 } catch (IllegalArgumentException e) { 292 if (options.inBitmap == null) throw e; 293 294 Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap"); 295 GalleryBitmapPool.getInstance().put(options.inBitmap); 296 options.inBitmap = null; 297 return decode(jc, fileDescriptor, options); 298 } 299 } 300 301 private static Bitmap findCachedBitmap(JobContext jc, byte[] data, 302 int offset, int length, Options options) { 303 decodeBounds(jc, data, offset, length, options); 304 return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight); 305 } 306 307 private static Bitmap findCachedBitmap(JobContext jc, FileDescriptor fileDescriptor, 308 Options options) { 309 decodeBounds(jc, fileDescriptor, options); 310 return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight); 311 } 312 } 313