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