package de.thedevstack.conversationsplus.utils; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.RectF; import android.media.ExifInterface; import android.net.Uri; import android.util.LruCache; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.exceptions.ImageResizeException; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.utils.ExifHelper; /** * This util provides */ public final class ImageUtil { private static int IMAGE_SIZE = 1920; private static LruCache BITMAP_CACHE; /** * Returns a bitmap from the cache. * @see LruCache#get(Object) for details * @param key the key of the bitmap to get * @return the bitmap */ public static Bitmap getBitmapFromCache(String key) { return BITMAP_CACHE.get(key); } /** * Adds a bitmap with the given key to the cache. * @see LruCache#put(Object, Object) for details * @param key the key to identify this bitmap * @param bitmap the bitmap to cache */ public static void addBitmapToCache(String key, Bitmap bitmap) { BITMAP_CACHE.put(key, bitmap); } /** * Removes the bitmap with given key from the cache. * @param key the key of the bitmap to remove */ public static void removeBitmapFromCache(String key) { BITMAP_CACHE.remove(key); } /** * Clears the cache. * @see LruCache#evictAll() for more details. */ public static void evictBitmapCache() { BITMAP_CACHE.evictAll(); } /** * Initializes the bitmap cache. * This has to be executed once on application start. * @see LruCache#LruCache(int) for details */ public static void initBitmapCache() { Logging.i("Conversations+ImageUtil", "Initializing BitmapCache"); final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; BITMAP_CACHE = new LruCache(cacheSize) { @Override protected int sizeOf(final String key, final Bitmap bitmap) { return bitmap.getByteCount() / 1024; } }; } /** * Resizes a given bitmap and return a new and resized bitmap. * The bitmap is only resized if either the width or the height of the original bitmap is smaller than the given size. * @param originalBitmap the bitmap to resize * @param size the size to scale to * @return new and resized bitmap or the original bitmap if width and height are smaller than size */ public static Bitmap resize(Bitmap originalBitmap, int size) { int w = originalBitmap.getWidth(); int h = originalBitmap.getHeight(); if (Math.max(w, h) > size) { int scalledW; int scalledH; if (w <= h) { scalledW = (int) (w / ((double) h / size)); scalledH = size; } else { scalledW = size; scalledH = (int) (h / ((double) w / size)); } return Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); } else { return originalBitmap; } } /** * Resizes and rotates an image given by uri and returns the bitmap. * @param image the uri of the image to be resized and rotated * @return resized and rotated bitmap * @throws ImageResizeException */ public static Bitmap resizeAndRotateImage(Uri image) throws ImageResizeException { return ImageUtil.resizeAndRotateImage(image, 0); } /** * Resizes and rotates an image given by uri and returns the bitmap. * @param image the uri of the image to be resized and rotated * @return resized and rotated bitmap * @throws ImageResizeException */ private static Bitmap resizeAndRotateImage(Uri image, int sampleSize) throws ImageResizeException { InputStream imageInputStream = null; try { imageInputStream = StreamUtil.openInputStreamFromContentResolver(image); Bitmap originalBitmap; BitmapFactory.Options options = new BitmapFactory.Options(); int inSampleSize = (int) Math.pow(2, sampleSize); Logging.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize); options.inSampleSize = inSampleSize; originalBitmap = BitmapFactory.decodeStream(imageInputStream, null, options); imageInputStream.close(); if (originalBitmap == null) { throw new ImageResizeException(R.string.error_not_an_image_file); } Bitmap scaledBitmap = ImageUtil.resize(originalBitmap, IMAGE_SIZE); int rotation = ImageUtil.getRotation(image); if (rotation > 0) { scaledBitmap = ImageUtil.rotate(scaledBitmap, rotation); } return scaledBitmap; } catch (FileNotFoundException e) { throw new ImageResizeException(R.string.error_file_not_found); } catch (IOException e) { throw new ImageResizeException(R.string.error_io_exception); } catch (OutOfMemoryError e) { ++sampleSize; if (sampleSize <= 3) { return resizeAndRotateImage(image, sampleSize); } else { throw new ImageResizeException(R.string.error_out_of_memory); } } finally { StreamUtil.close(imageInputStream); } } /** * Returns the rotation from the exif information of an image identified with the given uri. * The orientation is retrieved by parsing the stream of the image. * FileNotFoundException is silently ignored. * @param image the uri of the image to get the rotation * @return the rotation value for the image, 0 if the file cannot be found. */ public static int getRotation(Uri image) { InputStream is = null; try { is = StreamUtil.openInputStreamFromContentResolver(image); return ExifHelper.getOrientation(is); } catch (FileNotFoundException e) { return 0; } finally { StreamUtil.close(is); } } /** * Returns a thumbnail for a bitmap in a message. * @param message the message to get the thumbnail for * @param size the size to resize the original image to * @param cacheOnly whether only cached images should be returned or not * @return the resized thumbail * @throws FileNotFoundException if the original image does not exist anymore or an IOException occurs. */ public static Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException { Bitmap thumbnail = ImageUtil.getBitmapFromCache(message.getUuid()); if ((thumbnail == null) && (!cacheOnly)) { File file = FileBackend.getFile(message); BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(file, size); Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options); if (fullsize == null) { throw new FileNotFoundException(); } thumbnail = resize(fullsize, size); thumbnail = rotate(thumbnail, file.getAbsolutePath()); ImageUtil.addBitmapToCache(message.getUuid(), thumbnail); } return thumbnail; } /** * Rotates an bitmap. Only the values 90°, 180° and 270° are considered to rotate the image. * The orientation information is read using the ExifInterface. * @param original the original bitmap * @param srcPath the path to the original bitmap (used to read the exif information) * @return rotated bitmap, or original bitmap if criteria are not met * @throws IOException */ public static Bitmap rotate(Bitmap original, String srcPath) { try { ExifInterface exif = new ExifInterface(srcPath); int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); int rotation = 0; switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: rotation = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: rotation = 180; break; case ExifInterface.ORIENTATION_ROTATE_270: rotation = 270; break; } if (rotation > 0) { return rotate(original, rotation); } } catch (IOException e) { Logging.w("filebackend", "Error while rotating image, returning original (" + e.getMessage() + ")"); } return original; } /** * Rotates a bitmap with given degrees. * @param bitmap the bitmap to be rotated * @param degree the degrees to rotate the bitmap * @return a newly created bitmap */ public static Bitmap rotate(Bitmap bitmap, int degree) { int w = bitmap.getWidth(); int h = bitmap.getHeight(); Matrix mtx = new Matrix(); mtx.postRotate(degree); return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); } public static Bitmap cropCenterSquare(Uri image, int size) { if (image == null) { return null; } InputStream is = null; try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(image, size); is = StreamUtil.openInputStreamFromContentResolver(image); Bitmap input = BitmapFactory.decodeStream(is, null, options); if (input == null) { return null; } else { int rotation = getRotation(image); if (rotation > 0) { input = rotate(input, rotation); } return cropCenterSquare(input, size); } } catch (FileNotFoundException e) { return null; } finally { StreamUtil.close(is); } } public static Bitmap cropCenter(Uri image, int newHeight, int newWidth) { if (image == null) { return null; } InputStream is = null; try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(image,Math.max(newHeight, newWidth)); is = StreamUtil.openInputStreamFromContentResolver(image); Bitmap source = BitmapFactory.decodeStream(is, null, options); if (source == null) { return null; } int sourceWidth = source.getWidth(); int sourceHeight = source.getHeight(); float xScale = (float) newWidth / sourceWidth; float yScale = (float) newHeight / sourceHeight; float scale = Math.max(xScale, yScale); float scaledWidth = scale * sourceWidth; float scaledHeight = scale * sourceHeight; float left = (newWidth - scaledWidth) / 2; float top = (newHeight - scaledHeight) / 2; RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(dest); canvas.drawBitmap(source, null, targetRect, null); return dest; } catch (FileNotFoundException e) { return null; } finally { StreamUtil.close(is); } } public static Bitmap cropCenterSquare(Bitmap input, int size) { int w = input.getWidth(); int h = input.getHeight(); float scale = Math.max((float) size / h, (float) size / w); float outWidth = scale * w; float outHeight = scale * h; float left = (size - outWidth) / 2; float top = (size - outHeight) / 2; RectF target = new RectF(left, top, left + outWidth, top + outHeight); Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(output); canvas.drawBitmap(input, null, target, null); return output; } public static int calcSampleSize(Uri image, int size) throws FileNotFoundException { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeStream(StreamUtil.openInputStreamFromContentResolver(image), null, options); int height = options.outHeight; int width = options.outWidth; if (null != bmp) { bmp.recycle(); } return calcSampleSize(width, height, size); } public static int calcSampleSize(File image, int size) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeFile(image.getAbsolutePath(), options); int height = options.outHeight; int width = options.outWidth; if (null != bmp) { bmp.recycle(); } return calcSampleSize(width, height, size); } private static int calcSampleSize(int width, int height, int size) { int inSampleSize = 1; if (height > size || width > size) { int halfHeight = height / 2; int halfWidth = width / 2; while ((halfHeight / inSampleSize) > size && (halfWidth / inSampleSize) > size) { inSampleSize *= 2; } } return inSampleSize; } private ImageUtil() { // Static helper class } }