/*
* Copyright 2015 Daniel Dittmar
*
* 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 dan.dit.whatsthat.util.image;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* An image utiliy class that can save an image to a file and
* do other helpful stuff for image reconstruction.
* @author Daniel
*
*/
public final class ImageUtil {
private static final String IMAGE_FILE_PREFIX = "WTH_";
private static final String IMAGE_FILE_EXTENSION = ".png";
private static final double SIMILARITY_SCALING_THRESHOLD = 0.5; // 0 would mean only exactly the same aspect ratio
private static final String MEDIA_DIRECTORY_NAME = "WhatsThat Media";
private static MessageDigest DIGEST;
public static final ImageCache CACHE = new ImageCache();
private ImageUtil() {
}
public static boolean saveToFile(Bitmap image, File target, Bitmap.CompressFormat format, int compression) {
if (image == null || target == null) {
return false;
}
if (format == null) {
// try to set format by file extension, else take .png but do not rename target file
String name = target.getName();
String pathLowerCase = name.toLowerCase();
if (pathLowerCase.endsWith(".png")) {
format = Bitmap.CompressFormat.PNG;
} else if (pathLowerCase.endsWith(".jpg") || pathLowerCase.endsWith(".jpeg")) {
format = Bitmap.CompressFormat.JPEG;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
&& pathLowerCase.endsWith(".webp")) {
format = Bitmap.CompressFormat.WEBP;
} else {
format = Bitmap.CompressFormat.PNG;
target = new File(target.getParentFile(), ensureFileExtension(target.getName(), ".png"));
}
}
if (compression < 0) {
compression = 0;
}
if (compression > 100) {
compression = 100;
}
boolean success = false;
try {
FileOutputStream fos = new FileOutputStream(target);
success=image.compress(format, compression, fos);
fos.close();
} catch (FileNotFoundException e) {
Log.e("Image", "File not found: " + e.getMessage());
} catch (IOException e) {
Log.e("Image", "Error accessing file: " + e.getMessage());
}
return success;
}
/**
* Saves the given image to the given file. If the name doesn't imply an image format
* @param context An optional context required to broadcast the successful saving of the media file to the system.
* @param image The image to be saved.
* @param fileName null-ok; The basic part of the output file name.
* @return A valid file if the image was successfully saved,
* if context or image parameter is <code>null</code> or there was an error accessing the external storage
* or saving the file this returns <code>null</code>.
*/
public static File saveToMediaFile(@Nullable Context context, Bitmap image, String fileName) {
// create new File for the new Image
if (image == null) {
return null;
}
File pictureFile;
if (TextUtils.isEmpty(fileName)) {
pictureFile = getOutputMediaFile(fileName);
} else {
pictureFile = new File(getMediaDirectory(), ensureFileExtension(fileName, getFileExtension(fileName)));
}
if (pictureFile == null) {
Log.e("Image",
"Error creating media file, check storage permissions: ");// e.getMessage());
return null;
}
boolean result = saveToFile(image, pictureFile, null, 100);
//broadcast to system to make media directory and file known
if (context != null && result) {
try {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri uri = Uri.fromFile(pictureFile);
mediaScanIntent.setData(uri);
context.sendBroadcast(mediaScanIntent);
} catch (Exception e) {
Log.e("Image", "Error broadcasting file " + pictureFile + ": " + e);
}
}
return result ? pictureFile : null;
}
public static File getMediaDirectory() {
String path = ExternalStorage.getExternalStoragePathIfMounted(MEDIA_DIRECTORY_NAME);
if (path == null) {
return null; // external storage not available
}
File mediaStorageDir = new File(path);
// This location works best if you want the created images to be shared
// between applications and persist after your app has been uninstalled.
// Create the storage directory if it does not exist
if (!mediaStorageDir.mkdirs() && !mediaStorageDir.isDirectory()){
return null;
}
return mediaStorageDir;
}
/* Create a File for saving a png image */
private static File getOutputMediaFile(String pImageName){
File mediaStorageDir = getMediaDirectory();
if (mediaStorageDir == null) {
return null;
}
// Create a media file name
File mediaFile;
String suffix = "";
if (!TextUtils.isEmpty(pImageName) && !pImageName.toLowerCase().endsWith(IMAGE_FILE_EXTENSION)) {
suffix = IMAGE_FILE_EXTENSION; // a 'valid' name, only missing the extension
} else if (TextUtils.isEmpty(pImageName)) {
String timeStamp = SimpleDateFormat.getDateTimeInstance().format(new Date());
suffix = timeStamp + IMAGE_FILE_EXTENSION; // empty name given
} // imageName=".png" still possible at this point if given image name was ".png"
int counter = 0;
do {
mediaFile = new File(mediaStorageDir.getPath() + File.separator
+ (counter == 0 ? "" : (counter + "_")) + pImageName + suffix);
counter++;
} while (mediaFile.exists() && counter < Integer.MAX_VALUE);
return mediaFile;
}
/**
* Returns a MD5 hash of the given data.
* @param data Data to hash, not null.
* @return The MD5 hash or null on error.
*/
public static String getHash(byte[] data) {
if (data == null) {
return null;
}
if (DIGEST == null) {
try {
DIGEST = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
Log.e("Util", "NoSuchAlgorithm HD5!");
return null;
}
}
DIGEST.reset();
DIGEST.update(data, 0, data.length);
return new BigInteger(1, DIGEST.digest()).toString(16); // length 32 in hex format
}
// convertDpToPixel(25f, metrics) -> (25dp converted to pixels)
public static float convertDpToPixel(float dp, DisplayMetrics metrics){
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
}
public static float convertDpToPixel(float dp, int screenDensity) {
return dp * screenDensity / 160.f;
}
public static float convertPixelsToDp(float px, DisplayMetrics metrics){
float dp = px / (metrics.densityDpi / 160.f);
return dp;
}
// Code from http://developer.android.com/training/displaying-bitmaps/load-bitmap.html
private static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
protected static boolean areAspectRatiosSimilar(int width1, int height1, int width2, int height2) {
// using absolute differences of the aspect ratios where height and width are interchangable
// similarity of 0 means that the aspect ratios are equal
// examples: 9:16 to 3:4 => similarity = 0.583 , ok'ish
// 5:3 to 2:4 => similarity = 3.033 , bad
// 16:9 to 15:10 => similarity = 0.341 , good
double similarity = Math.abs(height1 / ((double) width1) * width2 / ((double) height2) - 1.0)
+ Math.abs(width1 / ((double) height1) * height2 / ((double) width2) - 1.0);
return similarity <= SIMILARITY_SCALING_THRESHOLD;
}
public static boolean isAspectRatioSquareSimilar(int width, int height) {
// If r = width/height, T = SIMILARITY_SCALING_THRESHOLD this is true if (sqrt(T*T+4)/2 - T/2) <= r <= (T/2 + sqrt(T*T+4)/2)
return areAspectRatiosSimilar(width, height, 1, 1);
}
public static Bitmap loadBitmap(InputStream input, int reqWidth, int reqHeight, int mode) {
if (reqWidth <= 0 || reqHeight <= 0) {
// load unscaled image
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
return BitmapFactory.decodeStream(input, null, options);
}
final BitmapFactory.Options options = new BitmapFactory.Options();
if (input.markSupported()) {
// First decode with inJustDecodeBounds=true to check dimensions
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(input, null, options);
try {
input.reset();
} catch (IOException ioe) {
Log.e("Image", "Error resetting input stream when decoding image: " + ioe);
return null;
}
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
CACHE.addInBitmapOptions(options);
}
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap result = BitmapFactory.decodeStream(input, null, options);
if (result == null) {
return null;
}
return BitmapUtil.attemptBitmapScaling(result, reqWidth, reqHeight, mode);
}
public static Bitmap[] loadFrames(Resources res, int reqWidth, int reqHeight, int mode,
int... resIds) {
Bitmap[] frames = new Bitmap[resIds.length];
for (int i = 0; i < resIds.length; i++) {
frames[i] = loadBitmap(res, resIds[i], reqWidth, reqHeight, mode);
}
return frames;
}
public static Bitmap loadBitmap(Resources res, int resId, int reqWidth, int reqHeight, int mode) {
if (reqWidth <= 0 || reqHeight <= 0) {
// load unscaled image
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
return BitmapFactory.decodeResource(res, resId, options);
}
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
CACHE.addInBitmapOptions(options);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
Bitmap result = decodeResourceSave(res, resId, options);
if (result == null) {
return null;
}
return BitmapUtil.attemptBitmapScaling(result, reqWidth, reqHeight, mode);
}
private static Bitmap decodeResourceSave(Resources res, int resId, BitmapFactory.Options
options) {
try {
return BitmapFactory.decodeResource(res, resId, options);
} catch (IllegalArgumentException e) {
// if it failed because of the strange behavior of inBitmap retry once without this
// option)
if (options.inBitmap != null) {
options.inBitmap = null;
return decodeResourceSave(res, resId, options);
}
throw e;
}
}
/**
* Loads the bitmap specified by the given resource id. A negative value or zero for the required
* height or width will result in loading the unscaled original image.
* @param res Resources of the context.
* @param resId The resource id of the bitmap.
* @param reqWidth The required width of the image to load.
* @param reqHeight The required height of the image to load.
* @param enforceDimension If true mode is BitmapUtil's FIT_EXACT else mode is FIT_INSIDE_GENEROUS
* @return A bitmap that will approximate the given dimensions at its best or null if no bitmap could be loaded.
*/
public static Bitmap loadBitmap(Resources res, int resId, int reqWidth, int reqHeight, boolean enforceDimension) {
return loadBitmap(res, resId, reqWidth, reqHeight, enforceDimension ? BitmapUtil.MODE_FIT_EXACT : BitmapUtil.MODE_FIT_INSIDE_GENEROUS);
}
/**
* Loads the bitmap specified by the given path.
* @param path The path to the image.
* @param reqWidth The maximum width.
* @param reqHeight The maximum height.
* @param enforceDimension If true mode is BitmapUtil's FIT_EXACT else mode is FIT_INSIDE_GENEROUS
* @return A bitmap or null if no bitmap could be loaded or is not found.
*/
public static Bitmap loadBitmap(File path, int reqWidth, int reqHeight, boolean enforceDimension) {
return loadBitmap(path, reqWidth, reqHeight, enforceDimension ? BitmapUtil.MODE_FIT_EXACT : BitmapUtil.MODE_FIT_INSIDE_GENEROUS);
}
public static Bitmap loadBitmap(File path, int reqWidth, int reqHeight, int mode) {
if (reqWidth <= 0 || reqHeight <= 0) {
// load unscaled image
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
return BitmapFactory.decodeFile(path.getAbsolutePath(), options);
}
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path.getAbsolutePath(), options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
CACHE.addInBitmapOptions(options);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap result = BitmapFactory.decodeFile(path.getAbsolutePath(), options);
if (result == null) {
return null;
}
return BitmapUtil.attemptBitmapScaling(result, reqWidth, reqHeight, mode);
}
/**
* Gets the resource id corresponding to the drawable with the given name.
* @param context A context.
* @param drawableName The drawable resource name.
* @return The resource id to the drawable with the given name.
*/
public static int getDrawableResIdFromName(Context context, String drawableName) {
return context.getResources().getIdentifier(drawableName, "drawable", context.getPackageName());
}
/**
* Gets the resource name corresponding to the drawable with the given resource id. See
* getDrawableResIdFromName(Context,String).
* @param res A resource.
* @param resId The resource id of the drawable.
* @return The entry name of the resource.
*/
public static String getDrawableNameFromResId(Resources res, int resId) {
return res.getResourceEntryName(resId);
}
private static final String[] EXTENSIONS = new String[] {".jpeg", ".jpg", ".bmp", ".gif", ".png"};
private static String getFileExtension(String name) {
if (name == null) {
return null;
}
for (String ext : EXTENSIONS) {
if (name.endsWith(ext)) {
return ext;
}
}
return IMAGE_FILE_EXTENSION; // default extension
}
public static String ensureFileExtension(String imageName, String ensureExtension) {
if (TextUtils.isEmpty(imageName)) {
return ensureExtension;
}
String lowercaseName = imageName.toLowerCase();
if (lowercaseName.endsWith(ensureExtension)) {
return imageName;
}
for (String ext : EXTENSIONS) {
if (lowercaseName.endsWith(ext) && imageName.length() >= ext.length()) {
return imageName.substring(0, imageName.length() - ext.length()) + ensureExtension;
}
}
// had other extension or none, just add it
return imageName + ensureExtension;
}
//Library: https://code.google.com/p/pngj/wiki/Overview
//OR WITH STANDARD JAVA FOR WRITING PNG METADATA
/*RenderedImage image = getMyImage();
Iterator<ImageWriter> iterator = ImageIO.getImageWritersBySuffix( "png" );
if(!iterator.hasNext()) throw new Error( "No image writer for PNG" );
ImageWriter imagewriter = iterator.next();
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
imagewriter.setOutput( ImageIO.createImageOutputStream( bytes ) );
// Create & populate metadata
PNGMetadata metadata = new PNGMetadata();
// see http://www.w3.org/TR/PNG-Chunks.html#C.tEXt for standardized keywords
metadata.tEXt_keyword.add( "Title" );
metadata.tEXt_text.add( "Mandelbrot" );
metadata.tEXt_keyword.add( "Comment" );
metadata.tEXt_text.add( "..." );
metadata.tEXt_keyword.add( "MandelbrotCoords" ); // custom keyword
metadata.tEXt_text.add( fractal.getCoords().toString() );
// Render the PNG to memory
IIOImage iioImage = new IIOImage( image, null, null );
iioImage.setMetadata( metadata ); // Attach the metadata
imagewriter.write( null, iioImage, null );
writer.dispose();*/
}