package de.jeisfeld.augendiagnoselib.util.imagefile;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.LightingColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.media.ExifInterface;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import de.jeisfeld.augendiagnoselib.Application;
import de.jeisfeld.augendiagnoselib.R;
import de.jeisfeld.augendiagnoselib.components.OverlayPinchImageView;
import de.jeisfeld.augendiagnoselib.util.DateUtil;
/**
* Utility class for operations with images.
*/
public final class ImageUtil {
// JAVADOC:OFF
// Rotation angles
private static final int ROTATION_90 = 90;
private static final int ROTATION_180 = 180;
private static final int ROTATION_270 = 270;
// JAVADOC:ON
/**
* The size of the mesh used to deform the overlays.
*/
private static final int OVERLAY_MESH_SIZE = 128;
/**
* Number of milliseconds for retry of getting bitmap.
*/
private static final long BITMAP_RETRY = 50;
/**
* The file endings considered as image files.
*/
private static final List<String> IMAGE_SUFFIXES = Arrays.asList("JPG", "JPEG", "PNG", "BMP", "TIF", "TIFF", "GIF");
/**
* The precision for saving jpeg of views.
*/
private static final int JPEG_PRECISION = 95;
/**
* The max size of a byte.
*/
private static final int BYTE = 255;
/**
* Hide default constructor.
*/
private ImageUtil() {
throw new UnsupportedOperationException();
}
/**
* Get the date field with the EXIF date from the file If not existing, use the last modified date.
*
* @param path The file path of the image
* @return the date stored in the EXIF data.
*/
@Nullable
public static Date getExifDate(@NonNull final String path) {
Date retrievedDate = null;
try {
ExifInterface exif = new ExifInterface(path);
String dateString = exif.getAttribute(ExifInterface.TAG_DATETIME);
if (dateString == null) {
dateString = JpegMetadataUtil.getExifDate(new File(path));
}
retrievedDate = DateUtil.parse(dateString, "yyyy:MM:dd HH:mm:ss");
}
catch (Exception e) {
Log.w(Application.TAG, e.toString() + " - Cannot retrieve EXIF date for " + path);
}
if (retrievedDate == null) {
File f = new File(path);
retrievedDate = new Date(f.lastModified());
}
return retrievedDate;
}
/**
* Retrieve the rotation angle from the Exif data of an image.
*
* @param path The file path of the image
* @return the rotation stored in the exif data, mapped into degrees.
*/
private static int getExifRotation(@NonNull final String path) {
int rotation = 0;
try {
ExifInterface exif = new ExifInterface(path);
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
if (orientation == ExifInterface.ORIENTATION_UNDEFINED) {
// Use custom implementation, as the previous one is not always reliable
orientation = JpegMetadataUtil.getExifOrientation(new File(path));
}
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_270:
rotation = ROTATION_270;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
rotation = ROTATION_180;
break;
case ExifInterface.ORIENTATION_ROTATE_90:
rotation = ROTATION_90;
break;
default:
break;
}
}
catch (Exception e) {
Log.w(Application.TAG, "Exception when getting EXIF rotation");
}
return rotation;
}
/**
* Return a bitmap of this photo.
*
* @param path The file path of the image.
* @param maxSize The maximum size of this bitmap. If bigger, it will be resized.
* @return the bitmap.
*/
@Nullable
public static Bitmap getImageBitmap(@NonNull final String path, final int maxSize) {
Bitmap bitmap = null;
if (maxSize <= 0) {
bitmap = BitmapFactory.decodeFile(path);
}
else {
if (maxSize <= MediaStoreUtil.MINI_THUMB_SIZE) {
bitmap = MediaStoreUtil.getThumbnailFromPath(path, maxSize);
}
if (bitmap == null) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = getBitmapFactor(path, maxSize);
// options.inPurgeable = true;
bitmap = BitmapFactory.decodeFile(path, options);
if (bitmap == null) {
// cannot create bitmap - try once more in case that the image was just in process of saving
// metadata
try {
Thread.sleep(BITMAP_RETRY);
}
catch (InterruptedException e) {
// ignore exception
}
bitmap = BitmapFactory.decodeFile(path, options);
if (bitmap == null) {
// cannot create bitmap - return dummy
Log.w(Application.TAG, "Cannot create bitmap from path " + path + " - return dummy bitmap");
return getDummyBitmap();
}
}
}
if (bitmap.getWidth() == 0 || bitmap.getHeight() == 0) {
return bitmap;
}
if (bitmap.getWidth() > maxSize || bitmap.getHeight() > maxSize
|| maxSize <= MediaStoreUtil.MINI_THUMB_SIZE) {
// Only if bitmap is bigger than maxSize, then resize it - but don't trust the thumbs from media store.
if (bitmap.getWidth() > bitmap.getHeight()) {
int targetWidth = maxSize;
int targetHeight = bitmap.getHeight() * maxSize / bitmap.getWidth();
bitmap = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false);
}
else {
int targetWidth = bitmap.getWidth() * maxSize / bitmap.getHeight();
int targetHeight = maxSize;
bitmap = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false);
}
}
}
int rotation = getExifRotation(path);
if (rotation != 0) {
bitmap = rotateBitmap(bitmap, rotation);
}
return bitmap;
}
/**
* Return a bitmap of a photo directly from byte array data.
*
* @param data The byte array data of the bitmap.
* @param maxSize The maximum size of this bitmap. If bigger, it will be resized.
* @return the bitmap.
*/
public static Bitmap getImageBitmap(@NonNull final byte[] data, final int maxSize) {
Bitmap bitmap;
if (maxSize <= 0) {
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
}
else {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = getBitmapFactor(data, maxSize);
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
bitmap = resizeBitmap(bitmap, maxSize, false);
}
return bitmap;
}
/**
* Resize a bitmap to the given size.
*
* @param baseBitmap The original bitmap.
* @param targetSize The target size.
* @param allowGrowing flag indicating if the image is allowed to grow.
* @return the resized image.
*/
public static Bitmap resizeBitmap(final Bitmap baseBitmap, final int targetSize, final boolean allowGrowing) {
if (baseBitmap.getWidth() == 0 || baseBitmap.getHeight() == 0) {
return baseBitmap;
}
else if (baseBitmap.getWidth() > targetSize || baseBitmap.getHeight() > targetSize || allowGrowing) {
if (baseBitmap.getWidth() > baseBitmap.getHeight()) {
int targetWidth = targetSize;
int targetHeight = baseBitmap.getHeight() * targetSize / baseBitmap.getWidth();
return Bitmap.createScaledBitmap(baseBitmap, targetWidth, targetHeight, false);
}
else {
int targetWidth = baseBitmap.getWidth() * targetSize / baseBitmap.getHeight();
int targetHeight = targetSize;
return Bitmap.createScaledBitmap(baseBitmap, targetWidth, targetHeight, false);
}
}
else {
return baseBitmap;
}
}
/**
* Retrieve a part of a bitmap in full resolution.
*
* @param fullBitmap The bitmap from which to get the part.
* @param minX The minimum X position to retrieve.
* @param maxX The maximum X position to retrieve.
* @param minY The minimum Y position to retrieve.
* @param maxY The maximum Y position to retrieve.
* @return The bitmap.
*/
public static Bitmap getPartialBitmap(@NonNull final Bitmap fullBitmap, final float minX, final float maxX,
final float minY,
final float maxY) {
return Bitmap.createBitmap(fullBitmap, Math.round(minX * fullBitmap.getWidth()),
Math.round(minY * fullBitmap.getHeight()),
Math.round((maxX - minX) * fullBitmap.getWidth()),
Math.round((maxY - minY) * fullBitmap.getHeight()));
}
/**
* Utility to retrieve the sample size for BitmapFactory.decodeFile.
*
* @param filepath the path of the bitmap.
* @param targetSize the target size of the bitmap
* @return the sample size to be used.
*/
private static int getBitmapFactor(final String filepath, final int targetSize) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filepath, options);
int size = Math.max(options.outWidth, options.outHeight);
return size / targetSize;
}
/**
* Utility to retrieve the sample size for BitmapFactory.decodeFile.
*
* @param data the data of the bitmap.
* @param targetSize the target size of the bitmap
* @return the sample size to be used.
*/
private static int getBitmapFactor(@NonNull final byte[] data, final int targetSize) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, options);
int size = Math.max(options.outWidth, options.outHeight);
return size / targetSize;
}
/**
* Rotate a bitmap.
*
* @param source The original bitmap
* @param angle The rotation angle
* @return the rotated bitmap.
*/
private static Bitmap rotateBitmap(@NonNull final Bitmap source, final float angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
}
/**
* Update contrast and brightness of a bitmap.
*
* @param bmp input bitmap
* @param contrast 0..infinity - 1 is default
* @param brightness -1..1 - 0 is default
* @param saturation 1/3..infinity - 1 is default
* @param colorTemperature -1..1 - 0 is default
* @return new bitmap
*/
public static Bitmap changeBitmapColors(@NonNull final Bitmap bmp, final float contrast, final float brightness,
final float saturation, final float colorTemperature) {
if (contrast == 1 && brightness == 0 && saturation == 1 && colorTemperature == 0) {
return bmp;
}
// some baseCalculations for the mapping matrix
int temperatureColor = ImageUtil.convertTemperatureToColor(colorTemperature);
float factorRed = (float) BYTE / Color.red(temperatureColor);
float factorGreen = (float) BYTE / Color.green(temperatureColor);
float factorBlue = (float) BYTE / Color.blue(temperatureColor);
float correctionFactor = (float) Math.pow(factorRed * factorGreen * factorBlue, -1f / 3); // MAGIC_NUMBER
factorRed *= correctionFactor * contrast;
factorGreen *= correctionFactor * contrast;
factorBlue *= correctionFactor * contrast;
float offset = BYTE / 2f * (1 - contrast + brightness * contrast + brightness);
float oppositeSaturation = (1 - saturation) / 2;
ColorMatrix cm = new ColorMatrix(new float[]{ //
factorRed * saturation, factorGreen * oppositeSaturation, factorBlue * oppositeSaturation, 0, offset, //
factorRed * oppositeSaturation, factorGreen * saturation, factorBlue * oppositeSaturation, 0, offset, //
factorRed * oppositeSaturation, factorGreen * oppositeSaturation, factorBlue * saturation, 0, offset, //
0, 0, 0, 1, 0});
Bitmap ret = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), bmp.getConfig());
Canvas canvas = new Canvas(ret);
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(cm));
canvas.drawBitmap(bmp, 0, 0, paint);
return ret;
}
/**
* Convert a temperature into a color value representing the color of this temperature.
*
* @param temperature The temperature value (in the range -1..1).
* @return The color value.
*/
private static int convertTemperatureToColor(final double temperature) {
if (temperature >= 0) {
return Color.rgb((int) (BYTE - 150 * temperature), (int) (BYTE - 105 * temperature), BYTE); // MAGIC_NUMBER
}
else {
return Color.rgb(BYTE, (int) (BYTE + 80 * temperature), (int) (BYTE + 145 * temperature)); // MAGIC_NUMBER
}
}
/**
* Get Mime type from URI.
*
* @param uri The URI
* @return the mime type.
*/
@Nullable
public static String getMimeType(@NonNull final Uri uri) {
ContentResolver contentResolver = Application.getAppContext().getContentResolver();
String mimeType = contentResolver.getType(uri);
if (mimeType == null) {
String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
if (extension != null) {
extension = extension.toLowerCase(Locale.getDefault());
}
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mimeType == null) {
mimeType = "unknown";
}
}
return mimeType;
}
/**
* Check if a file is an image file.
*
* @param file The file
* @param strict if true, then the file content will be checked, otherwise the suffix is sufficient.
* @return true if it is an image file.
*/
private static boolean isImage(@Nullable final File file, final boolean strict) {
if (file == null || !file.exists() || file.isDirectory()) {
return false;
}
if (!strict) {
String fileName = file.getName();
int index = fileName.lastIndexOf('.');
if (index >= 0) {
String suffix = fileName.substring(index + 1);
if (IMAGE_SUFFIXES.contains(suffix.toUpperCase(Locale.getDefault()))) {
return true;
}
}
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getPath(), options);
return options.outWidth >= 0 - 1 && options.outHeight >= 0;
}
/**
* Get the list of image files in a folder.
*
* @param folderName The folder name.
* @return The list of image files in this folder.
*/
@NonNull
public static ArrayList<String> getImagesInFolder(@Nullable final String folderName) {
ArrayList<String> fileNames = new ArrayList<>();
if (folderName == null) {
return fileNames;
}
File folder = new File(folderName);
if (!folder.exists() || !folder.isDirectory()) {
return fileNames;
}
File[] imageFiles = folder.listFiles(new FileFilter() {
@Override
public boolean accept(final File file) {
return isImage(file, false);
}
});
if (imageFiles == null) {
return fileNames;
}
for (File file : imageFiles) {
fileNames.add(file.getAbsolutePath());
}
return fileNames;
}
/**
* Utility method to change a bitmap colour.
*
* @param sourceBitmap The original bitmap
* @param color The target color
* @return the bitmap with the target color.
*/
public static Bitmap changeBitmapColor(@NonNull final Bitmap sourceBitmap, final int color) {
Bitmap ret = Bitmap.createBitmap(sourceBitmap.getWidth(), sourceBitmap.getHeight(), sourceBitmap.getConfig());
Paint p = new Paint();
ColorFilter filter = new LightingColorFilter(0, color);
p.setAlpha(color >>> 24); // MAGIC_NUMBER
p.setColorFilter(filter);
Canvas canvas = new Canvas(ret);
canvas.drawBitmap(sourceBitmap, 0, 0, p);
return ret;
}
/**
* Deform the overlay bitmap according to a different pupil size.
*
* @param sourceBitmap The original overlay bitmap.
* @param origPupilSize The pupil size (relative to iris) in the original overlay bitmap.
* @param destPupilSize The pupil size (relative to iris) in the target overlay bitmap.
* @param pupilOffsetX The x offset of the pupil center, relative to the iris size
* @param pupilOffsetY The y offset of the pupil center, relative to the iris size
* @return The deformed overlay bitmap.
*/
public static Bitmap deformOverlayByPupilSize(@NonNull final Bitmap sourceBitmap, final float origPupilSize, final float destPupilSize,
@Nullable final Float pupilOffsetX, @Nullable final Float pupilOffsetY) {
if (origPupilSize == 0) {
// non-deformable overlay, such as pupil overlay.
return sourceBitmap;
}
int overlaySize = sourceBitmap.getWidth();
int overlayHalfSize = overlaySize / 2;
float irisRadius = overlayHalfSize * OverlayPinchImageView.OVERLAY_CIRCLE_RATIO;
// the center of enlargement
float targetCenterX = overlayHalfSize;
float targetCenterY = overlayHalfSize;
if (pupilOffsetX != null) {
targetCenterX += 2 * irisRadius * pupilOffsetX / (1 - destPupilSize);
}
if (pupilOffsetY != null) {
targetCenterY += 2 * irisRadius * pupilOffsetY / (1 - destPupilSize);
}
// Constants used for linear transformation of the iris part of the overlay.
float linTransB = (destPupilSize - origPupilSize) / (1 - origPupilSize);
float linTransM = (1 - destPupilSize) / (irisRadius * (1 - origPupilSize));
float absOrigPupilSize = origPupilSize * irisRadius;
Bitmap ret = Bitmap.createBitmap(overlaySize, overlaySize, sourceBitmap.getConfig());
Canvas canvas = new Canvas(ret);
float[] verts = new float[2 * (OVERLAY_MESH_SIZE + 1) * (OVERLAY_MESH_SIZE + 1)];
int vertsIndex = 0;
for (int y = 0; y <= OVERLAY_MESH_SIZE; y++) {
for (int x = 0; x <= OVERLAY_MESH_SIZE; x++) {
// The positions of the original mesh vertices in pixels relative to the center
float xPos = (float) x * overlaySize / OVERLAY_MESH_SIZE - overlayHalfSize;
float yPos = (float) y * overlaySize / OVERLAY_MESH_SIZE - overlayHalfSize;
float centerDist = (float) Math.sqrt(xPos * xPos + yPos * yPos);
if (centerDist >= irisRadius) {
// outside the iris, take original position
verts[vertsIndex++] = overlayHalfSize + xPos;
verts[vertsIndex++] = overlayHalfSize + yPos;
}
else if (centerDist == 0) {
verts[vertsIndex++] = targetCenterX;
verts[vertsIndex++] = targetCenterY;
}
else {
// original direction
float xDirection = xPos / centerDist;
float yDirection = yPos / centerDist;
// corresponding iris boundary point
float xBound = overlayHalfSize + xDirection * irisRadius;
float yBound = overlayHalfSize + yDirection * irisRadius;
float radialPosition = linTransM * centerDist + linTransB;
if (centerDist < absOrigPupilSize) {
radialPosition -= linTransB * Math.pow(1 - centerDist / absOrigPupilSize, 1.5f); // MAGIC_NUMBER
}
verts[vertsIndex++] = targetCenterX + (xBound - targetCenterX) * radialPosition;
verts[vertsIndex++] = targetCenterY + (yBound - targetCenterY) * radialPosition;
}
}
}
Paint paint = new Paint();
paint.setFilterBitmap(true);
canvas.drawBitmapMesh(sourceBitmap, OVERLAY_MESH_SIZE, OVERLAY_MESH_SIZE, verts, 0, null, 0, paint);
return ret;
}
/**
* Retrieves a dummy bitmap (for the case that an image file is not readable).
*
* @return the dummy bitmap.
*/
private static Bitmap getDummyBitmap() {
return BitmapFactory.decodeResource(Application.getAppContext().getResources(), R.drawable.cannot_read_image);
}
/**
* Store a bitmap in a temporary file and return the URL.
*
* @param bitmap the bitmap
* @param tempFileName The name of the temporary file.
* @return The URL.
*/
public static Uri getUriForFullResolutionBitmap(final Bitmap bitmap, final String tempFileName) {
if (bitmap == null) {
return null;
}
try {
File cachePath = new File(Application.getAppContext().getCacheDir(), "images");
cachePath.mkdirs();
File imageFile = new File(cachePath, tempFileName + ".jpg");
FileOutputStream stream = new FileOutputStream(imageFile);
bitmap.compress(CompressFormat.JPEG, JPEG_PRECISION, stream);
stream.close();
return FileProvider.getUriForFile(Application.getAppContext(), Application.getAppContext().getPackageName() + ".fileprovider", imageFile);
}
catch (IOException e) {
Log.e(Application.TAG, "Failed to store bitmap", e);
return null;
}
}
/**
* File filter class to identify image files.
*/
public static class ImageFileFilter implements FileFilter {
@Override
public final boolean accept(@NonNull final File file) {
Uri uri = Uri.fromFile(file);
return file.exists() && file.isFile() && ImageUtil.getMimeType(uri).startsWith("image/");
}
}
}