1 /*
2  * Copyright (C) 2013 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.photos;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.BitmapRegionDecoder;
25 import android.graphics.Canvas;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.net.Uri;
29 import android.opengl.GLUtils;
30 import android.os.Build;
31 import android.util.Log;
32 
33 import com.android.gallery3d.common.BitmapUtils;
34 import com.android.gallery3d.common.Utils;
35 import com.android.gallery3d.exif.ExifInterface;
36 import com.android.gallery3d.glrenderer.BasicTexture;
37 import com.android.gallery3d.glrenderer.BitmapTexture;
38 import com.android.photos.views.TiledImageRenderer;
39 
40 import java.io.BufferedInputStream;
41 import java.io.FileNotFoundException;
42 import java.io.IOException;
43 import java.io.InputStream;
44 
45 interface SimpleBitmapRegionDecoder {
getWidth()46     int getWidth();
getHeight()47     int getHeight();
decodeRegion(Rect wantRegion, BitmapFactory.Options options)48     Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options);
49 }
50 
51 class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder {
52     BitmapRegionDecoder mDecoder;
SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder)53     private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) {
54         mDecoder = decoder;
55     }
newInstance( String pathName, boolean isShareable)56     public static SimpleBitmapRegionDecoderWrapper newInstance(
57             String pathName, boolean isShareable) {
58         try {
59             BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable);
60             if (d != null) {
61                 return new SimpleBitmapRegionDecoderWrapper(d);
62             }
63         } catch (IOException e) {
64             Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e);
65             return null;
66         }
67         return null;
68     }
newInstance( InputStream is, boolean isShareable)69     public static SimpleBitmapRegionDecoderWrapper newInstance(
70             InputStream is, boolean isShareable) {
71         try {
72             BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable);
73             if (d != null) {
74                 return new SimpleBitmapRegionDecoderWrapper(d);
75             }
76         } catch (IOException e) {
77             Log.w("BitmapRegionTileSource", "getting decoder failed", e);
78             return null;
79         }
80         return null;
81     }
getWidth()82     public int getWidth() {
83         return mDecoder.getWidth();
84     }
getHeight()85     public int getHeight() {
86         return mDecoder.getHeight();
87     }
decodeRegion(Rect wantRegion, BitmapFactory.Options options)88     public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
89         return mDecoder.decodeRegion(wantRegion, options);
90     }
91 }
92 
93 class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder {
94     Bitmap mBuffer;
95     Canvas mTempCanvas;
96     Paint mTempPaint;
DumbBitmapRegionDecoder(Bitmap b)97     private DumbBitmapRegionDecoder(Bitmap b) {
98         mBuffer = b;
99     }
newInstance(String pathName)100     public static DumbBitmapRegionDecoder newInstance(String pathName) {
101         Bitmap b = BitmapFactory.decodeFile(pathName);
102         if (b != null) {
103             return new DumbBitmapRegionDecoder(b);
104         }
105         return null;
106     }
newInstance(InputStream is)107     public static DumbBitmapRegionDecoder newInstance(InputStream is) {
108         Bitmap b = BitmapFactory.decodeStream(is);
109         if (b != null) {
110             return new DumbBitmapRegionDecoder(b);
111         }
112         return null;
113     }
getWidth()114     public int getWidth() {
115         return mBuffer.getWidth();
116     }
getHeight()117     public int getHeight() {
118         return mBuffer.getHeight();
119     }
decodeRegion(Rect wantRegion, BitmapFactory.Options options)120     public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
121         if (mTempCanvas == null) {
122             mTempCanvas = new Canvas();
123             mTempPaint = new Paint();
124             mTempPaint.setFilterBitmap(true);
125         }
126         int sampleSize = Math.max(options.inSampleSize, 1);
127         Bitmap newBitmap = Bitmap.createBitmap(
128                 wantRegion.width() / sampleSize,
129                 wantRegion.height() / sampleSize,
130                 Bitmap.Config.ARGB_8888);
131         mTempCanvas.setBitmap(newBitmap);
132         mTempCanvas.save();
133         mTempCanvas.scale(1f / sampleSize, 1f / sampleSize);
134         mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint);
135         mTempCanvas.restore();
136         mTempCanvas.setBitmap(null);
137         return newBitmap;
138     }
139 }
140 
141 /**
142  * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
143  * {@link BitmapRegionDecoder} to wrap a local file
144  */
145 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
146 public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
147 
148     private static final String TAG = "BitmapRegionTileSource";
149 
150     private static final int GL_SIZE_LIMIT = 2048;
151     // This must be no larger than half the size of the GL_SIZE_LIMIT
152     // due to decodePreview being allowed to be up to 2x the size of the target
153     private static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
154 
155     public static abstract class BitmapSource {
156         private SimpleBitmapRegionDecoder mDecoder;
157         private Bitmap mPreview;
158         private int mRotation;
159         public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
160         private State mState = State.NOT_LOADED;
161 
162         /** Returns whether loading was successful. */
loadInBackground(InBitmapProvider bitmapProvider)163         public boolean loadInBackground(InBitmapProvider bitmapProvider) {
164             ExifInterface ei = new ExifInterface();
165             if (readExif(ei)) {
166                 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
167                 if (ori != null) {
168                     mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue());
169                 }
170             }
171             mDecoder = loadBitmapRegionDecoder();
172             if (mDecoder == null) {
173                 mState = State.ERROR_LOADING;
174                 return false;
175             } else {
176                 int width = mDecoder.getWidth();
177                 int height = mDecoder.getHeight();
178 
179                 BitmapFactory.Options opts = new BitmapFactory.Options();
180                 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
181                 opts.inPreferQualityOverSpeed = true;
182 
183                 float scale = (float) MAX_PREVIEW_SIZE / Math.max(width, height);
184                 opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
185                 opts.inJustDecodeBounds = false;
186                 opts.inMutable = true;
187 
188                 if (bitmapProvider != null) {
189                     int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize);
190                     Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles);
191                     if (reusableBitmap != null) {
192                         // Try loading with reusable bitmap
193                         opts.inBitmap = reusableBitmap;
194                         try {
195                             mPreview = loadPreviewBitmap(opts);
196                         } catch (IllegalArgumentException e) {
197                             Log.d(TAG, "Unable to reuse bitmap", e);
198                             opts.inBitmap = null;
199                             mPreview = null;
200                         }
201                     }
202                 }
203                 if (mPreview == null) {
204                     mPreview = loadPreviewBitmap(opts);
205                 }
206                 if (mPreview == null) {
207                     mState = State.ERROR_LOADING;
208                     return false;
209                 }
210 
211                 // Verify that the bitmap can be used on GL surface
212                 try {
213                     GLUtils.getInternalFormat(mPreview);
214                     GLUtils.getType(mPreview);
215                     mState = State.LOADED;
216                 } catch (IllegalArgumentException e) {
217                     Log.d(TAG, "Image cannot be rendered on a GL surface", e);
218                     mState = State.ERROR_LOADING;
219                 }
220                 return mState == State.LOADED;
221             }
222         }
223 
getLoadingState()224         public State getLoadingState() {
225             return mState;
226         }
227 
getBitmapRegionDecoder()228         public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
229             return mDecoder;
230         }
231 
getPreviewBitmap()232         public Bitmap getPreviewBitmap() {
233             return mPreview;
234         }
235 
getRotation()236         public int getRotation() {
237             return mRotation;
238         }
239 
readExif(ExifInterface ei)240         public abstract boolean readExif(ExifInterface ei);
loadBitmapRegionDecoder()241         public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
loadPreviewBitmap(BitmapFactory.Options options)242         public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
243 
244         public interface InBitmapProvider {
forPixelCount(int count)245             Bitmap forPixelCount(int count);
246         }
247     }
248 
249     public static class FilePathBitmapSource extends BitmapSource {
250         private String mPath;
FilePathBitmapSource(String path)251         public FilePathBitmapSource(String path) {
252             mPath = path;
253         }
254         @Override
loadBitmapRegionDecoder()255         public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
256             SimpleBitmapRegionDecoder d;
257             d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true);
258             if (d == null) {
259                 d = DumbBitmapRegionDecoder.newInstance(mPath);
260             }
261             return d;
262         }
263         @Override
loadPreviewBitmap(BitmapFactory.Options options)264         public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
265             return BitmapFactory.decodeFile(mPath, options);
266         }
267         @Override
readExif(ExifInterface ei)268         public boolean readExif(ExifInterface ei) {
269             try {
270                 ei.readExif(mPath);
271                 return true;
272             } catch (NullPointerException e) {
273                 Log.w("BitmapRegionTileSource", "reading exif failed", e);
274                 return false;
275             } catch (IOException e) {
276                 Log.w("BitmapRegionTileSource", "getting decoder failed", e);
277                 return false;
278             }
279         }
280     }
281 
282     public static class UriBitmapSource extends BitmapSource {
283         private Context mContext;
284         private Uri mUri;
UriBitmapSource(Context context, Uri uri)285         public UriBitmapSource(Context context, Uri uri) {
286             mContext = context;
287             mUri = uri;
288         }
regenerateInputStream()289         private InputStream regenerateInputStream() throws FileNotFoundException {
290             InputStream is = mContext.getContentResolver().openInputStream(mUri);
291             return new BufferedInputStream(is);
292         }
293         @Override
loadBitmapRegionDecoder()294         public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
295             try {
296                 InputStream is = regenerateInputStream();
297                 SimpleBitmapRegionDecoder regionDecoder =
298                         SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
299                 Utils.closeSilently(is);
300                 if (regionDecoder == null) {
301                     is = regenerateInputStream();
302                     regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
303                     Utils.closeSilently(is);
304                 }
305                 return regionDecoder;
306             } catch (FileNotFoundException e) {
307                 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
308                 return null;
309             }
310         }
311         @Override
loadPreviewBitmap(BitmapFactory.Options options)312         public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
313             try {
314                 InputStream is = regenerateInputStream();
315                 Bitmap b = BitmapFactory.decodeStream(is, null, options);
316                 Utils.closeSilently(is);
317                 return b;
318             } catch (FileNotFoundException | OutOfMemoryError e) {
319                 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
320                 return null;
321             }
322         }
323         @Override
readExif(ExifInterface ei)324         public boolean readExif(ExifInterface ei) {
325             InputStream is = null;
326             try {
327                 is = regenerateInputStream();
328                 ei.readExif(is);
329                 Utils.closeSilently(is);
330                 return true;
331             } catch (FileNotFoundException e) {
332                 Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
333                 return false;
334             } catch (IOException e) {
335                 Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
336                 return false;
337             } catch (NullPointerException e) {
338                 Log.d("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e);
339                 return false;
340             } finally {
341                 Utils.closeSilently(is);
342             }
343         }
344     }
345 
346     public static class ResourceBitmapSource extends BitmapSource {
347         private Resources mRes;
348         private int mResId;
ResourceBitmapSource(Resources res, int resId)349         public ResourceBitmapSource(Resources res, int resId) {
350             mRes = res;
351             mResId = resId;
352         }
regenerateInputStream()353         private InputStream regenerateInputStream() {
354             InputStream is = mRes.openRawResource(mResId);
355             return new BufferedInputStream(is);
356         }
357         @Override
loadBitmapRegionDecoder()358         public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
359             InputStream is = regenerateInputStream();
360             SimpleBitmapRegionDecoder regionDecoder =
361                     SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
362             Utils.closeSilently(is);
363             if (regionDecoder == null) {
364                 is = regenerateInputStream();
365                 regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
366                 Utils.closeSilently(is);
367             }
368             return regionDecoder;
369         }
370         @Override
loadPreviewBitmap(BitmapFactory.Options options)371         public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
372             return BitmapFactory.decodeResource(mRes, mResId, options);
373         }
374         @Override
readExif(ExifInterface ei)375         public boolean readExif(ExifInterface ei) {
376             try {
377                 InputStream is = regenerateInputStream();
378                 ei.readExif(is);
379                 Utils.closeSilently(is);
380                 return true;
381             } catch (IOException e) {
382                 Log.e("BitmapRegionTileSource", "Error reading resource", e);
383                 return false;
384             }
385         }
386     }
387 
388     SimpleBitmapRegionDecoder mDecoder;
389     int mWidth;
390     int mHeight;
391     int mTileSize;
392     private BasicTexture mPreview;
393     private final int mRotation;
394 
395     // For use only by getTile
396     private Rect mWantRegion = new Rect();
397     private BitmapFactory.Options mOptions;
398 
BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage)399     public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) {
400         mTileSize = TiledImageRenderer.suggestedTileSize(context);
401         mRotation = source.getRotation();
402         mDecoder = source.getBitmapRegionDecoder();
403         if (mDecoder != null) {
404             mWidth = mDecoder.getWidth();
405             mHeight = mDecoder.getHeight();
406             mOptions = new BitmapFactory.Options();
407             mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
408             mOptions.inPreferQualityOverSpeed = true;
409             mOptions.inTempStorage = tempStorage;
410 
411             Bitmap preview = source.getPreviewBitmap();
412             if (preview != null &&
413                     preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
414                     mPreview = new BitmapTexture(preview);
415             } else {
416                 Log.w(TAG, String.format(
417                         "Failed to create preview of apropriate size! "
418                         + " in: %dx%d, out: %dx%d",
419                         mWidth, mHeight,
420                         preview == null ? -1 : preview.getWidth(),
421                         preview == null ? -1 : preview.getHeight()));
422             }
423         }
424     }
425 
getBitmap()426     public Bitmap getBitmap() {
427         return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null;
428     }
429 
430     @Override
getTileSize()431     public int getTileSize() {
432         return mTileSize;
433     }
434 
435     @Override
getImageWidth()436     public int getImageWidth() {
437         return mWidth;
438     }
439 
440     @Override
getImageHeight()441     public int getImageHeight() {
442         return mHeight;
443     }
444 
445     @Override
getPreview()446     public BasicTexture getPreview() {
447         return mPreview;
448     }
449 
450     @Override
getRotation()451     public int getRotation() {
452         return mRotation;
453     }
454 
455     @Override
getTile(int level, int x, int y, Bitmap bitmap)456     public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
457         int tileSize = getTileSize();
458         int t = tileSize << level;
459         mWantRegion.set(x, y, x + t, y + t);
460 
461         if (bitmap == null) {
462             bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
463         }
464 
465         mOptions.inSampleSize = (1 << level);
466         mOptions.inBitmap = bitmap;
467 
468         try {
469             bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
470         } finally {
471             if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
472                 mOptions.inBitmap = null;
473             }
474         }
475 
476         if (bitmap == null) {
477             Log.w("BitmapRegionTileSource", "fail in decoding region");
478         }
479         return bitmap;
480     }
481 }
482