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.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.ExifOrientation;
     34 import com.android.gallery3d.common.Utils;
     35 import com.android.gallery3d.glrenderer.BasicTexture;
     36 import com.android.gallery3d.glrenderer.BitmapTexture;
     37 import com.android.photos.views.TiledImageRenderer;
     38 import com.android.wallpaperpicker.common.InputStreamProvider;
     39 
     40 import java.io.File;
     41 import java.io.IOException;
     42 import java.io.InputStream;
     43 
     44 interface SimpleBitmapRegionDecoder {
     45     int getWidth();
     46     int getHeight();
     47     Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options);
     48 }
     49 
     50 class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder {
     51     BitmapRegionDecoder mDecoder;
     52     private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) {
     53         mDecoder = decoder;
     54     }
     55 
     56     public static SimpleBitmapRegionDecoderWrapper newInstance(
     57             InputStream is, boolean isShareable) {
     58         try {
     59             BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable);
     60             if (d != null) {
     61                 return new SimpleBitmapRegionDecoderWrapper(d);
     62             }
     63         } catch (IOException e) {
     64             Log.w("BitmapRegionTileSource", "getting decoder failed", e);
     65             return null;
     66         }
     67         return null;
     68     }
     69     public int getWidth() {
     70         return mDecoder.getWidth();
     71     }
     72     public int getHeight() {
     73         return mDecoder.getHeight();
     74     }
     75     public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
     76         return mDecoder.decodeRegion(wantRegion, options);
     77     }
     78 }
     79 
     80 class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder {
     81     Bitmap mBuffer;
     82     Canvas mTempCanvas;
     83     Paint mTempPaint;
     84     private DumbBitmapRegionDecoder(Bitmap b) {
     85         mBuffer = b;
     86     }
     87     public static DumbBitmapRegionDecoder newInstance(InputStream is) {
     88         Bitmap b = BitmapFactory.decodeStream(is);
     89         if (b != null) {
     90             return new DumbBitmapRegionDecoder(b);
     91         }
     92         return null;
     93     }
     94     public int getWidth() {
     95         return mBuffer.getWidth();
     96     }
     97     public int getHeight() {
     98         return mBuffer.getHeight();
     99     }
    100     public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
    101         if (mTempCanvas == null) {
    102             mTempCanvas = new Canvas();
    103             mTempPaint = new Paint();
    104             mTempPaint.setFilterBitmap(true);
    105         }
    106         int sampleSize = Math.max(options.inSampleSize, 1);
    107         Bitmap newBitmap = Bitmap.createBitmap(
    108                 wantRegion.width() / sampleSize,
    109                 wantRegion.height() / sampleSize,
    110                 Bitmap.Config.ARGB_8888);
    111         mTempCanvas.setBitmap(newBitmap);
    112         mTempCanvas.save();
    113         mTempCanvas.scale(1f / sampleSize, 1f / sampleSize);
    114         mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint);
    115         mTempCanvas.restore();
    116         mTempCanvas.setBitmap(null);
    117         return newBitmap;
    118     }
    119 }
    120 
    121 /**
    122  * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
    123  * {@link BitmapRegionDecoder} to wrap a local file
    124  */
    125 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
    126 public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
    127 
    128     private static final String TAG = "BitmapRegionTileSource";
    129 
    130     private static final int GL_SIZE_LIMIT = 2048;
    131     // This must be no larger than half the size of the GL_SIZE_LIMIT
    132     // due to decodePreview being allowed to be up to 2x the size of the target
    133     private static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
    134 
    135     public static abstract class BitmapSource {
    136         private SimpleBitmapRegionDecoder mDecoder;
    137         private Bitmap mPreview;
    138         private int mRotation;
    139         public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
    140         private State mState = State.NOT_LOADED;
    141 
    142         /** Returns whether loading was successful. */
    143         public boolean loadInBackground(InBitmapProvider bitmapProvider) {
    144             mRotation = getExifRotation();
    145             mDecoder = loadBitmapRegionDecoder();
    146             if (mDecoder == null) {
    147                 mState = State.ERROR_LOADING;
    148                 return false;
    149             } else {
    150                 int width = mDecoder.getWidth();
    151                 int height = mDecoder.getHeight();
    152 
    153                 BitmapFactory.Options opts = new BitmapFactory.Options();
    154                 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
    155                 opts.inPreferQualityOverSpeed = true;
    156 
    157                 float scale = (float) MAX_PREVIEW_SIZE / Math.max(width, height);
    158                 opts.inSampleSize = Utils.computeSampleSizeLarger(scale);
    159                 opts.inJustDecodeBounds = false;
    160                 opts.inMutable = true;
    161 
    162                 if (bitmapProvider != null) {
    163                     int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize);
    164                     Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles);
    165                     if (reusableBitmap != null) {
    166                         // Try loading with reusable bitmap
    167                         opts.inBitmap = reusableBitmap;
    168                         try {
    169                             mPreview = loadPreviewBitmap(opts);
    170                         } catch (IllegalArgumentException e) {
    171                             Log.d(TAG, "Unable to reuse bitmap", e);
    172                             opts.inBitmap = null;
    173                             mPreview = null;
    174                         }
    175                     }
    176                 }
    177                 if (mPreview == null) {
    178                     mPreview = loadPreviewBitmap(opts);
    179                 }
    180                 if (mPreview == null) {
    181                     mState = State.ERROR_LOADING;
    182                     return false;
    183                 }
    184 
    185                 // Verify that the bitmap can be used on GL surface
    186                 try {
    187                     GLUtils.getInternalFormat(mPreview);
    188                     GLUtils.getType(mPreview);
    189                     mState = State.LOADED;
    190                 } catch (IllegalArgumentException e) {
    191                     Log.d(TAG, "Image cannot be rendered on a GL surface", e);
    192                     mState = State.ERROR_LOADING;
    193                 }
    194                 return mState == State.LOADED;
    195             }
    196         }
    197 
    198         public State getLoadingState() {
    199             return mState;
    200         }
    201 
    202         public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
    203             return mDecoder;
    204         }
    205 
    206         public Bitmap getPreviewBitmap() {
    207             return mPreview;
    208         }
    209 
    210         public int getRotation() {
    211             return mRotation;
    212         }
    213 
    214         public abstract int getExifRotation();
    215         public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
    216         public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
    217 
    218         public interface InBitmapProvider {
    219             Bitmap forPixelCount(int count);
    220         }
    221     }
    222 
    223     public static class InputStreamSource extends BitmapSource {
    224         private final InputStreamProvider mStreamProvider;
    225         private final Context mContext;
    226 
    227         public InputStreamSource(Context context, Uri uri) {
    228             this(InputStreamProvider.fromUri(context, uri), context);
    229         }
    230 
    231         public InputStreamSource(Resources res, int resId, Context context) {
    232             this(InputStreamProvider.fromResource(res, resId), context);
    233         }
    234 
    235         public InputStreamSource(InputStreamProvider streamProvider, Context context) {
    236             mStreamProvider = streamProvider;
    237             mContext = context;
    238         }
    239 
    240         @Override
    241         public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
    242             try {
    243                 InputStream is = mStreamProvider.newStreamNotNull();
    244                 SimpleBitmapRegionDecoder regionDecoder =
    245                         SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
    246                 Utils.closeSilently(is);
    247                 if (regionDecoder == null) {
    248                     is = mStreamProvider.newStreamNotNull();
    249                     regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
    250                     Utils.closeSilently(is);
    251                 }
    252                 return regionDecoder;
    253             } catch (IOException e) {
    254                 Log.e("InputStreamSource", "Failed to load stream", e);
    255                 return null;
    256             }
    257         }
    258 
    259         @Override
    260         public int getExifRotation() {
    261             return mStreamProvider.getRotationFromExif(mContext);
    262         }
    263 
    264         @Override
    265         public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
    266             try {
    267                 InputStream is = mStreamProvider.newStreamNotNull();
    268                 Bitmap b = BitmapFactory.decodeStream(is, null, options);
    269                 Utils.closeSilently(is);
    270                 return b;
    271             } catch (IOException | OutOfMemoryError e) {
    272                 Log.e("InputStreamSource", "Failed to load stream", e);
    273                 return null;
    274             }
    275         }
    276     }
    277 
    278     public static class FilePathBitmapSource extends InputStreamSource {
    279         private String mPath;
    280         public FilePathBitmapSource(File file, Context context) {
    281             super(context, Uri.fromFile(file));
    282             mPath = file.getAbsolutePath();
    283         }
    284 
    285         @Override
    286         public int getExifRotation() {
    287             return ExifOrientation.readRotation(mPath);
    288         }
    289     }
    290 
    291     SimpleBitmapRegionDecoder mDecoder;
    292     int mWidth;
    293     int mHeight;
    294     int mTileSize;
    295     private BasicTexture mPreview;
    296     private final int mRotation;
    297 
    298     // For use only by getTile
    299     private Rect mWantRegion = new Rect();
    300     private BitmapFactory.Options mOptions;
    301 
    302     public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) {
    303         mTileSize = TiledImageRenderer.suggestedTileSize(context);
    304         mRotation = source.getRotation();
    305         mDecoder = source.getBitmapRegionDecoder();
    306         if (mDecoder != null) {
    307             mWidth = mDecoder.getWidth();
    308             mHeight = mDecoder.getHeight();
    309             mOptions = new BitmapFactory.Options();
    310             mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
    311             mOptions.inPreferQualityOverSpeed = true;
    312             mOptions.inTempStorage = tempStorage;
    313 
    314             Bitmap preview = source.getPreviewBitmap();
    315             if (preview != null &&
    316                     preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
    317                     mPreview = new BitmapTexture(preview);
    318             } else {
    319                 Log.w(TAG, String.format(
    320                         "Failed to create preview of apropriate size! "
    321                         + " in: %dx%d, out: %dx%d",
    322                         mWidth, mHeight,
    323                         preview == null ? -1 : preview.getWidth(),
    324                         preview == null ? -1 : preview.getHeight()));
    325             }
    326         }
    327     }
    328 
    329     public Bitmap getBitmap() {
    330         return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null;
    331     }
    332 
    333     @Override
    334     public int getTileSize() {
    335         return mTileSize;
    336     }
    337 
    338     @Override
    339     public int getImageWidth() {
    340         return mWidth;
    341     }
    342 
    343     @Override
    344     public int getImageHeight() {
    345         return mHeight;
    346     }
    347 
    348     @Override
    349     public BasicTexture getPreview() {
    350         return mPreview;
    351     }
    352 
    353     @Override
    354     public int getRotation() {
    355         return mRotation;
    356     }
    357 
    358     @Override
    359     public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
    360         int tileSize = getTileSize();
    361         int t = tileSize << level;
    362         mWantRegion.set(x, y, x + t, y + t);
    363 
    364         if (bitmap == null) {
    365             bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
    366         }
    367 
    368         mOptions.inSampleSize = (1 << level);
    369         mOptions.inBitmap = bitmap;
    370 
    371         try {
    372             bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
    373         } finally {
    374             if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
    375                 mOptions.inBitmap = null;
    376             }
    377         }
    378 
    379         if (bitmap == null) {
    380             Log.w("BitmapRegionTileSource", "fail in decoding region");
    381         }
    382         return bitmap;
    383     }
    384 }
    385