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