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