/* * Copyright (C) 2012-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo Flow. * * Akvo Flow is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Akvo Flow is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Akvo Flow. If not, see <http://www.gnu.org/licenses/>. */ package org.akvo.flow.util; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.RectF; import android.media.ExifInterface; import android.text.TextUtils; import android.util.Base64; import android.util.DisplayMetrics; import android.view.ViewGroup; import android.widget.ImageView; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Field; import timber.log.Timber; public class ImageUtil { public static String encodeBase64(Bitmap bitmap, int reqWidth, int reqHeight) { Matrix m = new Matrix(); m.setRectToRect(new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()), new RectF(0, 0, reqWidth, reqHeight), Matrix.ScaleToFit.CENTER); Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true); ByteArrayOutputStream stream = new ByteArrayOutputStream(); resizedBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); byte[] image = stream.toByteArray(); return Base64.encodeToString(image, Base64.DEFAULT); } public static Bitmap decodeBase64(String data) { byte[] image = Base64.decode(data, Base64.DEFAULT); return BitmapFactory.decodeByteArray(image, 0, image.length); } /** * resizeImage handles resizing a too-large image file from the camera, * @return true if the image was successfully resized to the new file, false otherwise */ public static boolean resizeImage(String origFilename, String outFilename, int size) { int reqWidth, reqHeight; switch (size) { case ConstantUtil.IMAGE_SIZE_1280_960: reqWidth = 1280; reqHeight = 960; break; case ConstantUtil.IMAGE_SIZE_640_480: reqWidth = 640; reqHeight = 480; break; case ConstantUtil.IMAGE_SIZE_320_240: default: reqWidth = 320; reqHeight = 240; break; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(origFilename, options); // If image is in portrait mode, we swap the maximum width and height if (options.outHeight > options.outWidth) { int tmp = reqHeight; reqHeight = reqWidth; reqWidth = tmp; } Timber.d("Orig Image size: " + options.outWidth + "x" + options.outHeight); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeFile(origFilename, options); if (bitmap != null && saveImage(bitmap, outFilename)) { checkOrientation(origFilename, outFilename);// Ensure the EXIF data is not lost Timber.d("Resized Image size: " + bitmap.getWidth() + "x" + bitmap.getHeight()); return true; } return false; } private static void checkOrientation(String originalImage, String resizedImage) { try { ExifInterface exif1 = new ExifInterface(originalImage); ExifInterface exif2 = new ExifInterface(resizedImage); final String orientation1 = exif1.getAttribute(ExifInterface.TAG_ORIENTATION); final String orientation2 = exif2.getAttribute(ExifInterface.TAG_ORIENTATION); if (!TextUtils.isEmpty(orientation1) && !orientation1.equals(orientation2)) { Timber.d("Orientation property in EXIF does not match. Overriding it with original value..."); exif2.setAttribute(ExifInterface.TAG_ORIENTATION, orientation1); exif2.saveAttributes(); } } catch (IOException e) { Timber.e(e.getMessage()); } } public static float[] getLocation(String image) { try { ExifInterface exif = new ExifInterface(image); float[] output = new float[2]; if (exif.getLatLong(output)) { return output; } } catch (IOException e) { Timber.e(e.getMessage()); } return null; } public static boolean setLocation(String image, double latitude, double longitude) { try { ExifInterface exif = new ExifInterface(image); String latDMS = convertDMS(latitude); String lonDMS = convertDMS(longitude); String latRef = latitude >= 0d ? "N" : "S"; String lonRef = longitude >= 0d ? "E" : "W"; exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, latDMS); exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, latRef); exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, lonDMS); exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, lonRef); exif.saveAttributes(); return true; } catch (IOException e) { Timber.e(e.getMessage()); } return false; } private static boolean saveImage(Bitmap bitmap, String filename) { OutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(filename)); if (bitmap.compress(Bitmap.CompressFormat.JPEG, 75, out)) { return true; } } catch (FileNotFoundException e) { Timber.e(e.getMessage()); } finally { if (out != null) { try { out.close(); } catch (Exception ignored) {} } } return false; } /** * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates * the closest inSampleSize that will result in the final decoded bitmap having a width and * height equal to or larger than the requested width and height. This implementation does not * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but * results in a larger bitmap which isn't as useful for caching purposes. * * @param options An options object with out* params already populated (run through a decode* * method with inJustDecodeBounds==true * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap * @return The value to be used for inSampleSize */ 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) { // Calculate ratios of height and width to requested height and width final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // Choose the smallest ratio as inSampleSize value, this will guarantee a final image // with both dimensions larger than or equal to the requested height and width. inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; // This offers some additional logic in case the image has a strange // aspect ratio. For example, a panorama may have a much larger // width than height. In these cases the total pixels might still // end up being too large to fit comfortably in memory, so we should // be more aggressive with sample down the image (=larger inSampleSize). final float totalPixels = width * height; // Anything more than 2x the requested pixels we'll sample down further final float totalReqPixelsCap = reqWidth * reqHeight * 2; while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { inSampleSize++; } } return inSampleSize; } public static void displayImage(ImageView imageView, String filename) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filename, options); // Calculate inSampleSize final int[] size = getImageSize(imageView);// [width, height] options.inSampleSize = calculateInSampleSize(options, size[0], size[1]); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeFile(filename, options); if (bitmap != null) { Timber.d("Displaying image with inSampleSize: " + options.inSampleSize); imageView.setImageBitmap(bitmap); } } /** * Size computing algorithm: * 1) Get layout_width and layout_height. If both of them haven't exact value then go to step #2. * 2) Get maxWidth and maxHeight. If both of them are not set then go to step #3. * 3) Get device screen dimensions. */ public static int[] getImageSize(ImageView imageView) { DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics(); ViewGroup.LayoutParams params = imageView.getLayoutParams(); int width = params.width; // Get layout width parameter if (width <= 0) width = getFieldValue(imageView, "mMaxWidth"); // Check maxWidth parameter if (width <= 0) width = displayMetrics.widthPixels; int height = params.height; // Get layout height parameter if (height <= 0) height = getFieldValue(imageView, "mMaxHeight"); // Check maxHeight parameter if (height <= 0) height = displayMetrics.heightPixels; return new int[]{width, height}; } /** * Access the properties by Reflection. * * @param object * @param fieldName * @return */ private static int getFieldValue(Object object, String fieldName) { int value = 0; try { Field field = ImageView.class.getDeclaredField(fieldName); field.setAccessible(true); int fieldValue = (Integer) field.get(object); if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) { value = fieldValue; } } catch (Exception e) { Timber.e(e.getMessage()); } return value; } private static String convertDMS(double coordinate) { if (coordinate < -180.0 || coordinate > 180.0 || Double.isNaN(coordinate)) { throw new IllegalArgumentException("coordinate=" + coordinate); } // Fixed denominator for seconds final int secondsDenom = 1000; StringBuilder sb = new StringBuilder(); coordinate = Math.abs(coordinate); int degrees = (int) Math.floor(coordinate); sb.append(degrees); sb.append("/1,"); coordinate -= degrees; coordinate *= 60.0; int minutes = (int) Math.floor(coordinate); sb.append(minutes); sb.append("/1,"); coordinate -= minutes; coordinate *= 60.0; coordinate *= secondsDenom; int secondsNum = (int) Math.floor(coordinate); sb.append(secondsNum); sb.append("/").append(secondsDenom); return sb.toString(); } }