/*
* This source is part of the
* _____ ___ ____
* __ / / _ \/ _ | / __/___ _______ _
* / // / , _/ __ |/ _/_/ _ \/ __/ _ `/
* \___/_/|_/_/ |_/_/ (_)___/_/ \_, /
* /___/
* repository.
*
* Copyright (C) 2013 Benoit 'BoD' Lubek (BoD@JRAF.org)
* Copyright (C) 2013 Carmen Alvarez (c@rmen.ca)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jraf.android.util.bitmap;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Point;
import android.media.ExifInterface;
import android.util.Log;
import org.jraf.android.util.Constants;
public class BitmapUtil {
private static final String TAG = Constants.TAG + BitmapUtil.class.getSimpleName();
/**
* Call {@link BitmapFactory#decodeFile(String, android.graphics.BitmapFactory.Options)}, retrying up to 4 times with an increased
* {@link android.graphics.BitmapFactory.Options#inSampleSize} if an {@link OutOfMemoryError} occurs.<br/>
* If after trying 4 times the file still could not be decoded, {@code null} is returned.
*
* @param imageFile The file to be decoded.
* @param options The Options object passed to {@link BitmapFactory#decodeFile(String, android.graphics.BitmapFactory.Options)} (can be {@code null}).
* @return The decoded bitmap, or {@code null} if it could not be decoded.
*/
public static Bitmap tryDecodeFile(File imageFile, BitmapFactory.Options options) {
Log.d(TAG, "tryDecodeFile imageFile=" + imageFile);
int trials = 0;
while (trials < 4) {
try {
Bitmap res = BitmapFactory.decodeFile(imageFile.getPath(), options);
if (res == null) {
Log.d(TAG, "tryDecodeFile res=null");
} else {
Log.d(TAG, "tryDecodeFile res width=" + res.getWidth() + " height=" + res.getHeight());
}
return res;
} catch (OutOfMemoryError e) {
if (options == null) {
options = new BitmapFactory.Options();
options.inSampleSize = 1;
}
Log.w(TAG, "tryDecodeFile Could not decode file with inSampleSize=" + options.inSampleSize + ", try with inSampleSize="
+ (options.inSampleSize + 1), e);
options.inSampleSize++;
trials++;
}
}
Log.w(TAG, "tryDecodeFile Could not decode the file after " + trials + " trials, returning null");
return null;
}
/**
* Returns an mutable version of the given bitmap.<br/>
* The given bitmap is recycled. A temporary file is used (using {@link File#createTempFile(String, String)}) to avoid allocating twice the needed memory.
*/
public static Bitmap asMutable(Bitmap bitmap) throws IOException {
// This is the file going to use temporally to dump the bitmap bytes
File tmpFile = File.createTempFile(String.valueOf(System.currentTimeMillis()), null);
Log.d(TAG, "getImmutable tmpFile=" + tmpFile);
// Open it as an RandomAccessFile
RandomAccessFile randomAccessFile = new RandomAccessFile(tmpFile, "rw");
// Get the width and height of the source bitmap
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// Dump the bytes to the file.
// This assumes the source bitmap is loaded using options.inPreferredConfig = Config.ARGB_8888 (hence the value of 4 bytes per pixel)
FileChannel channel = randomAccessFile.getChannel();
MappedByteBuffer buffer = channel.map(MapMode.READ_WRITE, 0, width * height * 4);
bitmap.copyPixelsToBuffer(buffer);
// Recycle the source bitmap, this will be no longer used
bitmap.recycle();
// Create a new mutable bitmap to load the bitmap from the file
bitmap = Bitmap.createBitmap(width, height, bitmap.getConfig());
// Load it back from the temporary buffer
buffer.position(0);
bitmap.copyPixelsFromBuffer(buffer);
// Cleanup
channel.close();
randomAccessFile.close();
tmpFile.delete();
return bitmap;
}
/**
* List of EXIF tags used by {@link #copyExifTags(File, File)}.
*/
//@formatter:off
@SuppressLint("InlinedApi")
private static final String[] EXIF_TAGS = new String[] {
ExifInterface.TAG_APERTURE,
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_PROCESSING_METHOD,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_ISO,
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_WHITE_BALANCE,
};
//@formatter:on
/**
* Copy the EXIF tags from the source image file to the destination image file.
*
* @param sourceFile The existing source JPEG file.
* @param destFile The existing destination JPEG file.
* @throws IOException If EXIF information could not be read or written.
*/
public static void copyExifTags(File sourceFile, File destFile) throws IOException {
Log.d(TAG, "copyExifTags sourceFile=" + sourceFile + " destFile=" + destFile);
ExifInterface sourceExifInterface = new ExifInterface(sourceFile.getPath());
ExifInterface destExifInterface = new ExifInterface(destFile.getPath());
boolean atLeastOne = false;
for (String exifTag : EXIF_TAGS) {
String value = sourceExifInterface.getAttribute(exifTag);
if (value != null) {
atLeastOne = true;
destExifInterface.setAttribute(exifTag, value);
}
}
if (atLeastOne) destExifInterface.saveAttributes();
}
/**
* Retrieves the dimensions of the bitmap in the given file.
*
* @param bitmapFile The file containing the bitmap to measure.
* @return A {@code Point} containing the width in {@code x} and the height in {@code y}.
*/
public static Point getDimensions(File bitmapFile) {
Log.d(TAG, "getDimensions bitmapFile=" + bitmapFile);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(bitmapFile.getPath(), options);
int width = options.outWidth;
int height = options.outHeight;
Point res = new Point(width, height);
Log.d(TAG, "getDimensions res=" + res);
return res;
}
/**
* Retrieves the rotation in the EXIF tags of the given file.
*
* @param bitmapFile The file from which to retrieve the info.
* @return The rotation in degrees, or {@code 0} if there was no EXIF tags in the given file, or it could not be read.
*/
public static int getExifRotation(File bitmapFile) {
Log.d(TAG, "getExifRotation bitmapFile=" + bitmapFile);
ExifInterface exifInterface;
try {
exifInterface = new ExifInterface(bitmapFile.getPath());
} catch (IOException e) {
Log.w(TAG, "getExifRotation Could not read exif info: returning 0", e);
return 0;
}
int exifOrientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
Log.d(TAG, "getExifRotation orientation=" + exifOrientation);
int res = 0;
switch (exifOrientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
res = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
res = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
res = 270;
break;
}
Log.d(TAG, "getExifRotation res=" + res);
return res;
}
/**
* Creates a small version of the bitmap inside the given file, using the given max dimensions.<br/>
* The resulting bitmap's dimensions will always be smaller than the given max dimensions.<br/>
* The rotation EXIF tag of the given file, if present, is used to return a thumbnail that won't be rotated.
*
* @param bitmapFile The file containing the bitmap to create a thumbnail from.
* @param maxWidth The wanted maximum width of the resulting thumbnail.
* @param maxHeight The wanted maximum height of the resulting thumbnail.
* @return A small version of the bitmap, or (@code null} if the given bitmap could not be decoded.
*/
public static Bitmap createThumbnail(File bitmapFile, int maxWidth, int maxHeight) {
Log.d(TAG, "createThumbnail imageFile=" + bitmapFile + " maxWidth=" + maxWidth + " maxHeight=" + maxHeight);
// Get exif rotation
int rotation = getExifRotation(bitmapFile);
// Determine optimal inSampleSize
Point originalDimensions = getDimensions(bitmapFile);
int width = originalDimensions.x;
int height = originalDimensions.y;
int inSampleSize = 1;
if (rotation == 90 || rotation == 270) {
// In these 2 cases we invert the measured dimensions because the bitmap is rotated
width = originalDimensions.y;
height = originalDimensions.x;
}
int widthRatio = width / maxWidth;
int heightRatio = height / maxHeight;
// Take the max, because we don't care if one of the returned thumbnail's side is smaller
// than the specified maxWidth/maxHeight.
inSampleSize = Math.max(widthRatio, heightRatio);
Log.d(TAG, "createThumbnail using inSampleSize=" + inSampleSize);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;
Bitmap res = tryDecodeFile(bitmapFile, options);
if (res == null) {
Log.w(TAG, "createThumbnail Could not decode file, returning null");
return null;
}
// Rotate if necessary
if (rotation != 0) {
Log.d(TAG, "createThumbnail rotating thumbnail");
Matrix matrix = new Matrix();
matrix.postRotate(rotation);
Bitmap rotatedBitmap = null;
try {
rotatedBitmap = Bitmap.createBitmap(res, 0, 0, res.getWidth(), res.getHeight(), matrix, false);
res.recycle();
res = rotatedBitmap;
} catch (OutOfMemoryError exception) {
Log.w(TAG, "createThumbnail Could not rotate bitmap, keeping original orientation", exception);
}
}
Log.d(TAG, "createThumbnail res width=" + res.getWidth() + " height=" + res.getHeight());
return res;
}
}