1/*
2 * Copyright (C) 2012 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.gallery3d.filtershow.cache;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.res.Resources;
22import android.database.Cursor;
23import android.database.sqlite.SQLiteException;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.BitmapRegionDecoder;
27import android.graphics.Canvas;
28import android.graphics.Matrix;
29import android.graphics.Paint;
30import android.graphics.Rect;
31import android.net.Uri;
32import android.provider.MediaStore;
33import android.util.Log;
34import android.webkit.MimeTypeMap;
35
36import com.adobe.xmp.XMPException;
37import com.adobe.xmp.XMPMeta;
38import com.android.gallery3d.common.Utils;
39import com.android.gallery3d.exif.ExifInterface;
40import com.android.gallery3d.exif.ExifTag;
41import com.android.gallery3d.filtershow.imageshow.MasterImage;
42import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
43import com.android.gallery3d.filtershow.tools.XmpPresets;
44import com.android.gallery3d.util.XmpUtilHelper;
45
46import java.io.FileNotFoundException;
47import java.io.IOException;
48import java.io.InputStream;
49import java.util.List;
50
51public final class ImageLoader {
52
53    private static final String LOGTAG = "ImageLoader";
54
55    public static final String JPEG_MIME_TYPE = "image/jpeg";
56    public static final int DEFAULT_COMPRESS_QUALITY = 95;
57
58    public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT;
59    public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP;
60    public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT;
61    public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM;
62    public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT;
63    public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT;
64    public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP;
65    public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM;
66
67    private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
68    private static final float OVERDRAW_ZOOM = 1.2f;
69    private ImageLoader() {}
70
71    /**
72     * Returns the Mime type for a Url.  Safe to use with Urls that do not
73     * come from Gallery's content provider.
74     */
75    public static String getMimeType(Uri src) {
76        String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString());
77        String ret = null;
78        if (postfix != null) {
79            ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix);
80        }
81        return ret;
82    }
83
84    public static String getLocalPathFromUri(Context context, Uri uri) {
85        Cursor cursor = context.getContentResolver().query(uri,
86                new String[]{MediaStore.Images.Media.DATA}, null, null, null);
87        if (cursor == null) {
88            return null;
89        }
90        int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
91        cursor.moveToFirst();
92        return cursor.getString(index);
93    }
94
95    /**
96     * Returns the image's orientation flag.  Defaults to ORI_NORMAL if no valid
97     * orientation was found.
98     */
99    public static int getMetadataOrientation(Context context, Uri uri) {
100        if (uri == null || context == null) {
101            throw new IllegalArgumentException("bad argument to getOrientation");
102        }
103
104        // First try to find orientation data in Gallery's ContentProvider.
105        Cursor cursor = null;
106        try {
107            cursor = context.getContentResolver().query(uri,
108                    new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
109                    null, null, null);
110            if (cursor != null && cursor.moveToNext()) {
111                int ori = cursor.getInt(0);
112                switch (ori) {
113                    case 90:
114                        return ORI_ROTATE_90;
115                    case 270:
116                        return ORI_ROTATE_270;
117                    case 180:
118                        return ORI_ROTATE_180;
119                    default:
120                        return ORI_NORMAL;
121                }
122            }
123        } catch (SQLiteException e) {
124            // Do nothing
125        } catch (IllegalArgumentException e) {
126            // Do nothing
127        } catch (IllegalStateException e) {
128            // Do nothing
129        } finally {
130            Utils.closeSilently(cursor);
131        }
132        ExifInterface exif = new ExifInterface();
133        InputStream is = null;
134        // Fall back to checking EXIF tags in file or input stream.
135        try {
136            if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
137                String mimeType = getMimeType(uri);
138                if (!JPEG_MIME_TYPE.equals(mimeType)) {
139                    return ORI_NORMAL;
140                }
141                String path = uri.getPath();
142                exif.readExif(path);
143            } else {
144                is = context.getContentResolver().openInputStream(uri);
145                exif.readExif(is);
146            }
147            return parseExif(exif);
148        } catch (IOException e) {
149            Log.w(LOGTAG, "Failed to read EXIF orientation", e);
150        } finally {
151            try {
152                if (is != null) {
153                    is.close();
154                }
155            } catch (IOException e) {
156                Log.w(LOGTAG, "Failed to close InputStream", e);
157            }
158        }
159        return ORI_NORMAL;
160    }
161
162    private static int parseExif(ExifInterface exif){
163        Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
164        if (tagval != null) {
165            int orientation = tagval;
166            switch(orientation) {
167                case ORI_NORMAL:
168                case ORI_ROTATE_90:
169                case ORI_ROTATE_180:
170                case ORI_ROTATE_270:
171                case ORI_FLIP_HOR:
172                case ORI_FLIP_VERT:
173                case ORI_TRANSPOSE:
174                case ORI_TRANSVERSE:
175                    return orientation;
176                default:
177                    return ORI_NORMAL;
178            }
179        }
180        return ORI_NORMAL;
181    }
182
183    /**
184     * Returns the rotation of image at the given URI as one of 0, 90, 180,
185     * 270.  Defaults to 0.
186     */
187    public static int getMetadataRotation(Context context, Uri uri) {
188        int orientation = getMetadataOrientation(context, uri);
189        switch(orientation) {
190            case ORI_ROTATE_90:
191                return 90;
192            case ORI_ROTATE_180:
193                return 180;
194            case ORI_ROTATE_270:
195                return 270;
196            default:
197                return 0;
198        }
199    }
200
201    /**
202     * Takes an orientation and a bitmap, and returns the bitmap transformed
203     * to that orientation.
204     */
205    public static Bitmap orientBitmap(Bitmap bitmap, int ori) {
206        Matrix matrix = new Matrix();
207        int w = bitmap.getWidth();
208        int h = bitmap.getHeight();
209        if (ori == ORI_ROTATE_90 ||
210                ori == ORI_ROTATE_270 ||
211                ori == ORI_TRANSPOSE ||
212                ori == ORI_TRANSVERSE) {
213            int tmp = w;
214            w = h;
215            h = tmp;
216        }
217        switch (ori) {
218            case ORI_ROTATE_90:
219                matrix.setRotate(90, w / 2f, h / 2f);
220                break;
221            case ORI_ROTATE_180:
222                matrix.setRotate(180, w / 2f, h / 2f);
223                break;
224            case ORI_ROTATE_270:
225                matrix.setRotate(270, w / 2f, h / 2f);
226                break;
227            case ORI_FLIP_HOR:
228                matrix.preScale(-1, 1);
229                break;
230            case ORI_FLIP_VERT:
231                matrix.preScale(1, -1);
232                break;
233            case ORI_TRANSPOSE:
234                matrix.setRotate(90, w / 2f, h / 2f);
235                matrix.preScale(1, -1);
236                break;
237            case ORI_TRANSVERSE:
238                matrix.setRotate(270, w / 2f, h / 2f);
239                matrix.preScale(1, -1);
240                break;
241            case ORI_NORMAL:
242            default:
243                return bitmap;
244        }
245        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
246                bitmap.getHeight(), matrix, true);
247    }
248
249    /**
250     * Returns the bitmap for the rectangular region given by "bounds"
251     * if it is a subset of the bitmap stored at uri.  Otherwise returns
252     * null.
253     */
254    public static Bitmap loadRegionBitmap(Context context, BitmapCache cache,
255                                          Uri uri, BitmapFactory.Options options,
256                                          Rect bounds) {
257        InputStream is = null;
258        int w = 0;
259        int h = 0;
260        if (options.inSampleSize != 0) {
261            return null;
262        }
263        try {
264            is = context.getContentResolver().openInputStream(uri);
265            BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
266            Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
267            w = decoder.getWidth();
268            h = decoder.getHeight();
269            Rect imageBounds = new Rect(bounds);
270            // return null if bounds are not entirely within the bitmap
271            if (!r.contains(imageBounds)) {
272                imageBounds.intersect(r);
273                bounds.left = imageBounds.left;
274                bounds.top = imageBounds.top;
275            }
276            Bitmap reuse = cache.getBitmap(imageBounds.width(),
277                    imageBounds.height(), BitmapCache.REGION);
278            options.inBitmap = reuse;
279            Bitmap bitmap = decoder.decodeRegion(imageBounds, options);
280            if (bitmap != reuse) {
281                cache.cache(reuse); // not reused, put back in cache
282            }
283            return bitmap;
284        } catch (FileNotFoundException e) {
285            Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
286        } catch (IOException e) {
287            Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
288        } catch (IllegalArgumentException e) {
289            Log.e(LOGTAG, "exc, image decoded " + w + " x " + h + " bounds: "
290                    + bounds.left + "," + bounds.top + " - "
291                    + bounds.width() + "x" + bounds.height() + " exc: " + e);
292        } finally {
293            Utils.closeSilently(is);
294        }
295        return null;
296    }
297
298    /**
299     * Returns the bounds of the bitmap stored at a given Url.
300     */
301    public static Rect loadBitmapBounds(Context context, Uri uri) {
302        BitmapFactory.Options o = new BitmapFactory.Options();
303        o.inJustDecodeBounds = true;
304        loadBitmap(context, uri, o);
305        return new Rect(0, 0, o.outWidth, o.outHeight);
306    }
307
308    /**
309     * Loads a bitmap that has been downsampled using sampleSize from a given url.
310     */
311    public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) {
312        BitmapFactory.Options options = new BitmapFactory.Options();
313        options.inMutable = true;
314        options.inSampleSize = sampleSize;
315        return loadBitmap(context, uri, options);
316    }
317
318
319    /**
320     * Returns the bitmap from the given uri loaded using the given options.
321     * Returns null on failure.
322     */
323    public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) {
324        if (uri == null || context == null) {
325            throw new IllegalArgumentException("bad argument to loadBitmap");
326        }
327        InputStream is = null;
328        try {
329            is = context.getContentResolver().openInputStream(uri);
330            return BitmapFactory.decodeStream(is, null, o);
331        } catch (FileNotFoundException e) {
332            Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
333        } finally {
334            Utils.closeSilently(is);
335        }
336        return null;
337    }
338
339    /**
340     * Loads a bitmap at a given URI that is downsampled so that both sides are
341     * smaller than maxSideLength. The Bitmap's original dimensions are stored
342     * in the rect originalBounds.
343     *
344     * @param uri URI of image to open.
345     * @param context context whose ContentResolver to use.
346     * @param maxSideLength max side length of returned bitmap.
347     * @param originalBounds If not null, set to the actual bounds of the stored bitmap.
348     * @param useMin use min or max side of the original image
349     * @return downsampled bitmap or null if this operation failed.
350     */
351    public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength,
352            Rect originalBounds, boolean useMin) {
353        if (maxSideLength <= 0 || uri == null || context == null) {
354            throw new IllegalArgumentException("bad argument to getScaledBitmap");
355        }
356        // Get width and height of stored bitmap
357        Rect storedBounds = loadBitmapBounds(context, uri);
358        if (originalBounds != null) {
359            originalBounds.set(storedBounds);
360        }
361        int w = storedBounds.width();
362        int h = storedBounds.height();
363
364        // If bitmap cannot be decoded, return null
365        if (w <= 0 || h <= 0) {
366            return null;
367        }
368
369        // Find best downsampling size
370        int imageSide = 0;
371        if (useMin) {
372            imageSide = Math.min(w, h);
373        } else {
374            imageSide = Math.max(w, h);
375        }
376        int sampleSize = 1;
377        while (imageSide > maxSideLength) {
378            imageSide >>>= 1;
379            sampleSize <<= 1;
380        }
381
382        // Make sure sample size is reasonable
383        if (sampleSize <= 0 ||
384                0 >= (int) (Math.min(w, h) / sampleSize)) {
385            return null;
386        }
387        return loadDownsampledBitmap(context, uri, sampleSize);
388    }
389
390    /**
391     * Loads a bitmap at a given URI that is downsampled so that both sides are
392     * smaller than maxSideLength. The Bitmap's original dimensions are stored
393     * in the rect originalBounds.  The output is also transformed to the given
394     * orientation.
395     *
396     * @param uri URI of image to open.
397     * @param context context whose ContentResolver to use.
398     * @param maxSideLength max side length of returned bitmap.
399     * @param orientation  the orientation to transform the bitmap to.
400     * @param originalBounds set to the actual bounds of the stored bitmap.
401     * @return downsampled bitmap or null if this operation failed.
402     */
403    public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength,
404            int orientation, Rect originalBounds) {
405        Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false);
406        if (bmap != null) {
407            bmap = orientBitmap(bmap, orientation);
408            if (bmap.getConfig()!= Bitmap.Config.ARGB_8888){
409                bmap = bmap.copy( Bitmap.Config.ARGB_8888,true);
410            }
411        }
412        return bmap;
413    }
414
415    public static Bitmap getScaleOneImageForPreset(Context context,
416                                                   BitmapCache cache,
417                                                   Uri uri, Rect bounds,
418                                                   Rect destination) {
419        BitmapFactory.Options options = new BitmapFactory.Options();
420        options.inMutable = true;
421        if (destination != null) {
422            int thresholdWidth = (int) (destination.width() * OVERDRAW_ZOOM);
423            if (bounds.width() > thresholdWidth) {
424                int sampleSize = 1;
425                int w = bounds.width();
426                while (w > thresholdWidth) {
427                    sampleSize *= 2;
428                    w /= sampleSize;
429                }
430                options.inSampleSize = sampleSize;
431            }
432        }
433        return loadRegionBitmap(context, cache, uri, options, bounds);
434    }
435
436    /**
437     * Loads a bitmap that is downsampled by at least the input sample size. In
438     * low-memory situations, the bitmap may be downsampled further.
439     */
440    public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) {
441        boolean noBitmap = true;
442        int num_tries = 0;
443        if (sampleSize <= 0) {
444            sampleSize = 1;
445        }
446        Bitmap bmap = null;
447        while (noBitmap) {
448            try {
449                // Try to decode, downsample if low-memory.
450                bmap = loadDownsampledBitmap(context, sourceUri, sampleSize);
451                noBitmap = false;
452            } catch (java.lang.OutOfMemoryError e) {
453                // Try with more downsampling before failing for good.
454                if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
455                    throw e;
456                }
457                bmap = null;
458                System.gc();
459                sampleSize *= 2;
460            }
461        }
462        return bmap;
463    }
464
465    /**
466     * Loads an oriented bitmap that is downsampled by at least the input sample
467     * size. In low-memory situations, the bitmap may be downsampled further.
468     */
469    public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri,
470            int sampleSize) {
471        Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize);
472        if (bitmap == null) {
473            return null;
474        }
475        int orientation = getMetadataOrientation(context, sourceUri);
476        bitmap = orientBitmap(bitmap, orientation);
477        return bitmap;
478    }
479
480    /**
481     * Loads bitmap from a resource that may be downsampled in low-memory situations.
482     */
483    public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
484            int id) {
485        boolean noBitmap = true;
486        int num_tries = 0;
487        if (options.inSampleSize < 1) {
488            options.inSampleSize = 1;
489        }
490        // Stopgap fix for low-memory devices.
491        Bitmap bmap = null;
492        while (noBitmap) {
493            try {
494                // Try to decode, downsample if low-memory.
495                bmap = BitmapFactory.decodeResource(
496                        res, id, options);
497                noBitmap = false;
498            } catch (java.lang.OutOfMemoryError e) {
499                // Retry before failing for good.
500                if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
501                    throw e;
502                }
503                bmap = null;
504                System.gc();
505                options.inSampleSize *= 2;
506            }
507        }
508        return bmap;
509    }
510
511    public static XMPMeta getXmpObject(Context context) {
512        try {
513            InputStream is = context.getContentResolver().openInputStream(
514                    MasterImage.getImage().getUri());
515            return XmpUtilHelper.extractXMPMeta(is);
516        } catch (FileNotFoundException e) {
517            return null;
518        }
519    }
520
521    /**
522     * Determine if this is a light cycle 360 image
523     *
524     * @return true if it is a light Cycle image that is full 360
525     */
526    public static boolean queryLightCycle360(Context context) {
527        InputStream is = null;
528        try {
529            is = context.getContentResolver().openInputStream(MasterImage.getImage().getUri());
530            XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
531            if (meta == null) {
532                return false;
533            }
534            String namespace = "http://ns.google.com/photos/1.0/panorama/";
535            String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
536            String fullWidthName = "GPano:FullPanoWidthPixels";
537
538            if (!meta.doesPropertyExist(namespace, cropWidthName)) {
539                return false;
540            }
541            if (!meta.doesPropertyExist(namespace, fullWidthName)) {
542                return false;
543            }
544
545            Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
546            Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
547
548            // Definition of a 360:
549            // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
550            if (cropValue != null && fullValue != null) {
551                return cropValue.equals(fullValue);
552            }
553
554            return false;
555        } catch (FileNotFoundException e) {
556            return false;
557        } catch (XMPException e) {
558            return false;
559        } finally {
560            Utils.closeSilently(is);
561        }
562    }
563
564    public static List<ExifTag> getExif(Context context, Uri uri) {
565        String path = getLocalPathFromUri(context, uri);
566        if (path != null) {
567            Uri localUri = Uri.parse(path);
568            String mimeType = getMimeType(localUri);
569            if (!JPEG_MIME_TYPE.equals(mimeType)) {
570                return null;
571            }
572            try {
573                ExifInterface exif = new ExifInterface();
574                exif.readExif(path);
575                List<ExifTag> taglist = exif.getAllTags();
576                return taglist;
577            } catch (IOException e) {
578                Log.w(LOGTAG, "Failed to read EXIF tags", e);
579            }
580        }
581        return null;
582    }
583}
584