BitmapUtils.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. /*
  2. * ownCloud Android client application
  3. *
  4. * @author David A. Velasco
  5. * Copyright (C) 2015 ownCloud Inc.
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License version 2,
  9. * as published by the Free Software Foundation.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. package com.owncloud.android.utils;
  20. import android.content.res.Resources;
  21. import android.graphics.Bitmap;
  22. import android.graphics.BitmapFactory;
  23. import android.graphics.BitmapFactory.Options;
  24. import android.graphics.Canvas;
  25. import android.graphics.Matrix;
  26. import android.graphics.drawable.BitmapDrawable;
  27. import android.graphics.drawable.Drawable;
  28. import android.widget.ImageView;
  29. import com.owncloud.android.MainApp;
  30. import com.owncloud.android.R;
  31. import com.owncloud.android.lib.common.utils.Log_OC;
  32. import org.apache.commons.codec.binary.Hex;
  33. import java.nio.charset.Charset;
  34. import java.security.MessageDigest;
  35. import java.security.NoSuchAlgorithmException;
  36. import java.util.Locale;
  37. import androidx.annotation.Nullable;
  38. import androidx.core.graphics.drawable.RoundedBitmapDrawable;
  39. import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
  40. import androidx.exifinterface.media.ExifInterface;
  41. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  42. /**
  43. * Utility class with methods for decoding Bitmaps.
  44. */
  45. public final class BitmapUtils {
  46. public static final String TAG = BitmapUtils.class.getSimpleName();
  47. private BitmapUtils() {
  48. // utility class -> private constructor
  49. }
  50. /**
  51. * Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap will be drawn in a
  52. * surface of reqWidth x reqHeight
  53. *
  54. * @param srcPath Absolute path to the file containing the image.
  55. * @param reqWidth Width of the surface where the Bitmap will be drawn on, in pixels.
  56. * @param reqHeight Height of the surface where the Bitmap will be drawn on, in pixels.
  57. * @return decoded bitmap
  58. */
  59. public static Bitmap decodeSampledBitmapFromFile(String srcPath, int reqWidth, int reqHeight) {
  60. // set desired options that will affect the size of the bitmap
  61. final Options options = new Options();
  62. options.inScaled = true;
  63. options.inPurgeable = true;
  64. options.inPreferQualityOverSpeed = false;
  65. options.inMutable = false;
  66. // make a false load of the bitmap to get its dimensions
  67. options.inJustDecodeBounds = true;
  68. BitmapFactory.decodeFile(srcPath, options);
  69. // calculate factor to subsample the bitmap
  70. options.inSampleSize = calculateSampleFactor(options, reqWidth, reqHeight);
  71. // decode bitmap with inSampleSize set
  72. options.inJustDecodeBounds = false;
  73. return BitmapFactory.decodeFile(srcPath, options);
  74. }
  75. /**
  76. * Calculates a proper value for options.inSampleSize in order to decode a Bitmap minimizing the memory overload and
  77. * covering a target surface of reqWidth x reqHeight if the original image is big enough.
  78. *
  79. * @param options Bitmap decoding options; options.outHeight and options.inHeight should be set.
  80. * @param reqWidth Width of the surface where the Bitmap will be drawn on, in pixels.
  81. * @param reqHeight Height of the surface where the Bitmap will be drawn on, in pixels.
  82. * @return The largest inSampleSize value that is a power of 2 and keeps both height and width larger than reqWidth
  83. * and reqHeight.
  84. */
  85. public static int calculateSampleFactor(Options options, int reqWidth, int reqHeight) {
  86. final int height = options.outHeight;
  87. final int width = options.outWidth;
  88. int inSampleSize = 1;
  89. if (height > reqHeight || width > reqWidth) {
  90. final int halfHeight = height / 2;
  91. final int halfWidth = width / 2;
  92. // calculates the largest inSampleSize value (for smallest sample) that is a power of 2 and keeps both
  93. // height and width **larger** than the requested height and width.
  94. while ((halfHeight / inSampleSize) > reqHeight || (halfWidth / inSampleSize) > reqWidth) {
  95. inSampleSize *= 2;
  96. }
  97. }
  98. return inSampleSize;
  99. }
  100. /**
  101. * scales a given bitmap depending on the given size parameters.
  102. *
  103. * @param bitmap the bitmap to be scaled
  104. * @param px the target pixel size
  105. * @param width the width
  106. * @param height the height
  107. * @param max the max(height, width)
  108. * @return the scaled bitmap
  109. */
  110. public static Bitmap scaleBitmap(Bitmap bitmap, float px, int width, int height, int max) {
  111. float scale = px / max;
  112. int w = Math.round(scale * width);
  113. int h = Math.round(scale * height);
  114. return Bitmap.createScaledBitmap(bitmap, w, h, true);
  115. }
  116. /**
  117. * Rotate bitmap according to EXIF orientation. Cf. http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
  118. *
  119. * @param bitmap Bitmap to be rotated
  120. * @param storagePath Path to source file of bitmap. Needed for EXIF information.
  121. * @return correctly EXIF-rotated bitmap
  122. */
  123. public static Bitmap rotateImage(Bitmap bitmap, String storagePath) {
  124. Bitmap resultBitmap = bitmap;
  125. try {
  126. ExifInterface exifInterface = new ExifInterface(storagePath);
  127. int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1);
  128. Matrix matrix = new Matrix();
  129. // 1: nothing to do
  130. // 2
  131. if (orientation == ExifInterface.ORIENTATION_FLIP_HORIZONTAL) {
  132. matrix.postScale(-1.0f, 1.0f);
  133. }
  134. // 3
  135. else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
  136. matrix.postRotate(180);
  137. }
  138. // 4
  139. else if (orientation == ExifInterface.ORIENTATION_FLIP_VERTICAL) {
  140. matrix.postScale(1.0f, -1.0f);
  141. }
  142. // 5
  143. else if (orientation == ExifInterface.ORIENTATION_TRANSPOSE) {
  144. matrix.postRotate(-90);
  145. matrix.postScale(1.0f, -1.0f);
  146. }
  147. // 6
  148. else if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
  149. matrix.postRotate(90);
  150. }
  151. // 7
  152. else if (orientation == ExifInterface.ORIENTATION_TRANSVERSE) {
  153. matrix.postRotate(90);
  154. matrix.postScale(1.0f, -1.0f);
  155. }
  156. // 8
  157. else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
  158. matrix.postRotate(270);
  159. }
  160. // Rotate the bitmap
  161. resultBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
  162. if (!resultBitmap.equals(bitmap)) {
  163. bitmap.recycle();
  164. }
  165. } catch (Exception exception) {
  166. Log_OC.e("BitmapUtil", "Could not rotate the image: " + storagePath);
  167. }
  168. return resultBitmap;
  169. }
  170. public static Color usernameToColor(String name) throws NoSuchAlgorithmException {
  171. String hash = name.toLowerCase(Locale.ROOT);
  172. // already a md5 hash?
  173. if (!hash.matches("([0-9a-f]{4}-?){8}$")) {
  174. hash = md5(hash);
  175. }
  176. hash = hash.replaceAll("[^0-9a-f]", "");
  177. int steps = 6;
  178. Color[] finalPalette = generateColors(steps);
  179. return finalPalette[hashToInt(hash, steps * 3)];
  180. }
  181. private static int hashToInt(String hash, int maximum) {
  182. int finalInt = 0;
  183. int[] result = new int[hash.length()];
  184. // splitting evenly the string
  185. for (int i = 0; i < hash.length(); i++) {
  186. // chars in md5 goes up to f, hex: 16
  187. result[i] = Integer.parseInt(String.valueOf(hash.charAt(i)), 16) % 16;
  188. }
  189. // adds up all results
  190. for (int value : result) {
  191. finalInt += value;
  192. }
  193. // chars in md5 goes up to f, hex:16
  194. // make sure we're always using int in our operation
  195. return Integer.parseInt(String.valueOf(Integer.parseInt(String.valueOf(finalInt), 10) % maximum), 10);
  196. }
  197. private static Color[] generateColors(int steps) {
  198. Color red = new Color(182, 70, 157);
  199. Color yellow = new Color(221, 203, 85);
  200. Color blue = new Color(0, 130, 201); // Nextcloud blue
  201. Color[] palette1 = mixPalette(steps, red, yellow);
  202. Color[] palette2 = mixPalette(steps, yellow, blue);
  203. Color[] palette3 = mixPalette(steps, blue, red);
  204. Color[] resultPalette = new Color[palette1.length + palette2.length + palette3.length];
  205. System.arraycopy(palette1, 0, resultPalette, 0, palette1.length);
  206. System.arraycopy(palette2, 0, resultPalette, palette1.length, palette2.length);
  207. System.arraycopy(palette3,
  208. 0,
  209. resultPalette,
  210. palette1.length + palette2.length,
  211. palette1.length);
  212. return resultPalette;
  213. }
  214. @SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX")
  215. private static Color[] mixPalette(int steps, Color color1, Color color2) {
  216. Color[] palette = new Color[steps];
  217. palette[0] = color1;
  218. float[] step = stepCalc(steps, color1, color2);
  219. for (int i = 1; i < steps; i++) {
  220. int r = (int) (color1.r + step[0] * i);
  221. int g = (int) (color1.g + step[1] * i);
  222. int b = (int) (color1.b + step[2] * i);
  223. palette[i] = new Color(r, g, b);
  224. }
  225. return palette;
  226. }
  227. private static float[] stepCalc(int steps, Color color1, Color color2) {
  228. float[] step = new float[3];
  229. step[0] = (color2.r - color1.r) / (float) steps;
  230. step[1] = (color2.g - color1.g) / (float) steps;
  231. step[2] = (color2.b - color1.b) / (float) steps;
  232. return step;
  233. }
  234. public static class Color {
  235. public int r;
  236. public int g;
  237. public int b;
  238. public Color(int r, int g, int b) {
  239. this.r = r;
  240. this.g = g;
  241. this.b = b;
  242. }
  243. @Override
  244. public boolean equals(@Nullable Object obj) {
  245. if (!(obj instanceof Color)) {
  246. return false;
  247. }
  248. Color other = (Color) obj;
  249. return this.r == other.r && this.g == other.g && this.b == other.b;
  250. }
  251. @Override
  252. public int hashCode() {
  253. return r * 10000 + g * 1000 + b;
  254. }
  255. }
  256. public static String md5(String string) throws NoSuchAlgorithmException {
  257. MessageDigest md5 = MessageDigest.getInstance("MD5");
  258. md5.update(string.getBytes(Charset.defaultCharset()));
  259. return new String(Hex.encodeHex(md5.digest()));
  260. }
  261. /**
  262. * Returns a new circular bitmap drawable by creating it from a bitmap, setting initial target density based on the
  263. * display metrics of the resources.
  264. *
  265. * @param resources the resources for initial target density
  266. * @param bitmap the original bitmap
  267. * @return the circular bitmap
  268. */
  269. public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources,
  270. Bitmap bitmap,
  271. float radius) {
  272. if (bitmap == null) {
  273. return null;
  274. }
  275. RoundedBitmapDrawable roundedBitmap = RoundedBitmapDrawableFactory.create(resources, bitmap);
  276. roundedBitmap.setCircular(true);
  277. if (radius != -1) {
  278. roundedBitmap.setCornerRadius(radius);
  279. }
  280. return roundedBitmap;
  281. }
  282. public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources, Bitmap bitmap) {
  283. return bitmapToCircularBitmapDrawable(resources, bitmap, -1);
  284. }
  285. public static void setRoundedBitmap(Resources resources, Bitmap bitmap, float radius, ImageView imageView) {
  286. imageView.setImageDrawable(BitmapUtils.bitmapToCircularBitmapDrawable(resources,
  287. bitmap,
  288. radius));
  289. }
  290. public static Bitmap drawableToBitmap(Drawable drawable) {
  291. if (drawable instanceof BitmapDrawable) {
  292. BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
  293. if (bitmapDrawable.getBitmap() != null) {
  294. return bitmapDrawable.getBitmap();
  295. }
  296. }
  297. Bitmap bitmap;
  298. if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
  299. bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
  300. } else {
  301. bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
  302. Bitmap.Config.ARGB_8888);
  303. }
  304. Canvas canvas = new Canvas(bitmap);
  305. drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
  306. drawable.draw(canvas);
  307. return bitmap;
  308. }
  309. public static void setRoundedBitmap(Bitmap thumbnail, ImageView imageView) {
  310. BitmapUtils.setRoundedBitmap(getResources(),
  311. thumbnail,
  312. getResources().getDimension(R.dimen.file_icon_rounded_corner_radius),
  313. imageView);
  314. }
  315. public static void setRoundedBitmapForGridMode(Bitmap thumbnail, ImageView imageView) {
  316. BitmapUtils.setRoundedBitmap(getResources(),
  317. thumbnail,
  318. getResources().getDimension(R.dimen.file_icon_rounded_corner_radius_for_grid_mode),
  319. imageView);
  320. }
  321. private static Resources getResources() {
  322. return MainApp.getAppContext().getResources();
  323. }
  324. }