BitmapUtils.java 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. /*
  2. * Nextcloud - Android Client
  3. *
  4. * SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky <tobias@kaminsky.me>
  5. * SPDX-FileCopyrightText: 23017-2018 Andy Scherzinger <info@andy-scherzinger.de>
  6. * SPDX-FileCopyrightText: 2015 ownCloud Inc.
  7. * SPDX-FileCopyrightText: 2014 David A. Velasco <dvelasco@solidgear.es>
  8. * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
  9. */
  10. package com.owncloud.android.utils;
  11. import android.content.Context;
  12. import android.content.res.Resources;
  13. import android.graphics.Bitmap;
  14. import android.graphics.BitmapFactory;
  15. import android.graphics.BitmapFactory.Options;
  16. import android.graphics.Canvas;
  17. import android.graphics.ImageDecoder;
  18. import android.graphics.Matrix;
  19. import android.graphics.Paint;
  20. import android.graphics.PorterDuff;
  21. import android.graphics.PorterDuffColorFilter;
  22. import android.graphics.PorterDuffXfermode;
  23. import android.graphics.Rect;
  24. import android.graphics.RectF;
  25. import android.graphics.drawable.BitmapDrawable;
  26. import android.graphics.drawable.Drawable;
  27. import android.os.Build;
  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 com.owncloud.android.lib.resources.users.Status;
  33. import com.owncloud.android.lib.resources.users.StatusType;
  34. import com.owncloud.android.ui.StatusDrawable;
  35. import org.apache.commons.codec.binary.Hex;
  36. import java.io.File;
  37. import java.nio.charset.Charset;
  38. import java.security.MessageDigest;
  39. import java.security.NoSuchAlgorithmException;
  40. import java.util.Locale;
  41. import androidx.annotation.NonNull;
  42. import androidx.annotation.Nullable;
  43. import androidx.core.graphics.drawable.RoundedBitmapDrawable;
  44. import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
  45. import androidx.exifinterface.media.ExifInterface;
  46. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  47. /**
  48. * Utility class with methods for decoding Bitmaps.
  49. */
  50. public final class BitmapUtils {
  51. public static final String TAG = BitmapUtils.class.getSimpleName();
  52. private BitmapUtils() {
  53. // utility class -> private constructor
  54. }
  55. public static Bitmap addColorFilter(Bitmap originalBitmap, int filterColor, int opacity) {
  56. int width = originalBitmap.getWidth();
  57. int height = originalBitmap.getHeight();
  58. Bitmap resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
  59. Canvas canvas = new Canvas(resultBitmap);
  60. canvas.drawBitmap(originalBitmap, 0, 0, null);
  61. Paint paint = new Paint();
  62. paint.setColor(filterColor);
  63. paint.setAlpha(opacity);
  64. paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
  65. canvas.drawRect(0, 0, width, height, paint);
  66. return resultBitmap;
  67. }
  68. /**
  69. * Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap will be drawn in a
  70. * surface of reqWidth x reqHeight
  71. *
  72. * @param srcPath Absolute path to the file containing the image.
  73. * @param reqWidth Width of the surface where the Bitmap will be drawn on, in pixels.
  74. * @param reqHeight Height of the surface where the Bitmap will be drawn on, in pixels.
  75. * @return decoded bitmap
  76. */
  77. public static Bitmap decodeSampledBitmapFromFile(String srcPath, int reqWidth, int reqHeight) {
  78. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  79. // For API 28 and above, use ImageDecoder
  80. try {
  81. return ImageDecoder.decodeBitmap(ImageDecoder.createSource(new File(srcPath)),
  82. (decoder, info, source) -> {
  83. // Set the target size
  84. decoder.setTargetSize(reqWidth, reqHeight);
  85. });
  86. } catch (Exception exception) {
  87. Log_OC.e("BitmapUtil", "Error decoding the bitmap from file: " + srcPath + ", exception: " + exception.getMessage());
  88. }
  89. }
  90. // set desired options that will affect the size of the bitmap
  91. final Options options = new Options();
  92. // make a false load of the bitmap to get its dimensions
  93. options.inJustDecodeBounds = true;
  94. // FIXME after auto-rename can't generate thumbnail from localPath
  95. BitmapFactory.decodeFile(srcPath, options);
  96. // calculate factor to subsample the bitmap
  97. options.inSampleSize = calculateSampleFactor(options, reqWidth, reqHeight);
  98. // decode bitmap with inSampleSize set
  99. options.inJustDecodeBounds = false;
  100. return BitmapFactory.decodeFile(srcPath, options);
  101. }
  102. /**
  103. * Calculates a proper value for options.inSampleSize in order to decode a Bitmap minimizing the memory overload and
  104. * covering a target surface of reqWidth x reqHeight if the original image is big enough.
  105. *
  106. * @param options Bitmap decoding options; options.outHeight and options.inHeight should be set.
  107. * @param reqWidth Width of the surface where the Bitmap will be drawn on, in pixels.
  108. * @param reqHeight Height of the surface where the Bitmap will be drawn on, in pixels.
  109. * @return The largest inSampleSize value that is a power of 2 and keeps both height and width larger than reqWidth
  110. * and reqHeight.
  111. */
  112. public static int calculateSampleFactor(Options options, int reqWidth, int reqHeight) {
  113. final int height = options.outHeight;
  114. final int width = options.outWidth;
  115. int inSampleSize = 1;
  116. if (height > reqHeight || width > reqWidth) {
  117. final int halfHeight = height / 2;
  118. final int halfWidth = width / 2;
  119. // calculates the largest inSampleSize value (for smallest sample) that is a power of 2 and keeps both
  120. // height and width **larger** than the requested height and width.
  121. while ((halfHeight / inSampleSize) > reqHeight || (halfWidth / inSampleSize) > reqWidth) {
  122. inSampleSize *= 2;
  123. }
  124. }
  125. return inSampleSize;
  126. }
  127. /**
  128. * scales a given bitmap depending on the given size parameters.
  129. *
  130. * @param bitmap the bitmap to be scaled
  131. * @param px the target pixel size
  132. * @param width the width
  133. * @param height the height
  134. * @param max the max(height, width)
  135. * @return the scaled bitmap
  136. */
  137. public static Bitmap scaleBitmap(Bitmap bitmap, float px, int width, int height, int max) {
  138. float scale = px / max;
  139. int w = Math.round(scale * width);
  140. int h = Math.round(scale * height);
  141. return Bitmap.createScaledBitmap(bitmap, w, h, true);
  142. }
  143. /**
  144. * Rotate bitmap according to EXIF orientation. Cf. http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
  145. *
  146. * @param bitmap Bitmap to be rotated
  147. * @param storagePath Path to source file of bitmap. Needed for EXIF information.
  148. * @return correctly EXIF-rotated bitmap
  149. */
  150. public static Bitmap rotateImage(Bitmap bitmap, String storagePath) {
  151. Bitmap resultBitmap = bitmap;
  152. try {
  153. ExifInterface exifInterface = new ExifInterface(storagePath);
  154. int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1);
  155. if (orientation != ExifInterface.ORIENTATION_NORMAL) {
  156. Matrix matrix = new Matrix();
  157. switch (orientation) {
  158. // 2
  159. case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: {
  160. matrix.postScale(-1.0f, 1.0f);
  161. break;
  162. }
  163. // 3
  164. case ExifInterface.ORIENTATION_ROTATE_180: {
  165. matrix.postRotate(180);
  166. break;
  167. }
  168. // 4
  169. case ExifInterface.ORIENTATION_FLIP_VERTICAL: {
  170. matrix.postScale(1.0f, -1.0f);
  171. break;
  172. }
  173. // 5
  174. case ExifInterface.ORIENTATION_TRANSPOSE: {
  175. matrix.postRotate(-90);
  176. matrix.postScale(1.0f, -1.0f);
  177. break;
  178. }
  179. // 6
  180. case ExifInterface.ORIENTATION_ROTATE_90: {
  181. matrix.postRotate(90);
  182. break;
  183. }
  184. // 7
  185. case ExifInterface.ORIENTATION_TRANSVERSE: {
  186. matrix.postRotate(90);
  187. matrix.postScale(1.0f, -1.0f);
  188. break;
  189. }
  190. // 8
  191. case ExifInterface.ORIENTATION_ROTATE_270: {
  192. matrix.postRotate(270);
  193. break;
  194. }
  195. }
  196. // Rotate the bitmap
  197. resultBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
  198. if (!resultBitmap.equals(bitmap)) {
  199. bitmap.recycle();
  200. }
  201. }
  202. } catch (Exception exception) {
  203. Log_OC.e("BitmapUtil", "Could not rotate the image: " + storagePath);
  204. }
  205. return resultBitmap;
  206. }
  207. public static int[] getImageResolution(String srcPath) {
  208. Options options = new Options();
  209. options.inJustDecodeBounds = true;
  210. BitmapFactory.decodeFile(srcPath, options);
  211. return new int [] {options.outWidth, options.outHeight};
  212. }
  213. public static Color usernameToColor(String name) {
  214. String hash = name.toLowerCase(Locale.ROOT);
  215. // already a md5 hash?
  216. if (!hash.matches("([0-9a-f]{4}-?){8}$")) {
  217. try {
  218. hash = md5(hash);
  219. } catch (NoSuchAlgorithmException e) {
  220. int color = getResources().getColor(R.color.primary_dark);
  221. return new Color(android.graphics.Color.red(color),
  222. android.graphics.Color.green(color),
  223. android.graphics.Color.blue(color));
  224. }
  225. }
  226. hash = hash.replaceAll("[^0-9a-f]", "");
  227. int steps = 6;
  228. Color[] finalPalette = generateColors(steps);
  229. return finalPalette[hashToInt(hash, steps * 3)];
  230. }
  231. private static int hashToInt(String hash, int maximum) {
  232. int finalInt = 0;
  233. int[] result = new int[hash.length()];
  234. // splitting evenly the string
  235. for (int i = 0; i < hash.length(); i++) {
  236. // chars in md5 goes up to f, hex: 16
  237. result[i] = Integer.parseInt(String.valueOf(hash.charAt(i)), 16) % 16;
  238. }
  239. // adds up all results
  240. for (int value : result) {
  241. finalInt += value;
  242. }
  243. // chars in md5 goes up to f, hex:16
  244. // make sure we're always using int in our operation
  245. return Integer.parseInt(String.valueOf(Integer.parseInt(String.valueOf(finalInt), 10) % maximum), 10);
  246. }
  247. private static Color[] generateColors(int steps) {
  248. Color red = new Color(182, 70, 157);
  249. Color yellow = new Color(221, 203, 85);
  250. Color blue = new Color(0, 130, 201); // Nextcloud blue
  251. Color[] palette1 = mixPalette(steps, red, yellow);
  252. Color[] palette2 = mixPalette(steps, yellow, blue);
  253. Color[] palette3 = mixPalette(steps, blue, red);
  254. Color[] resultPalette = new Color[palette1.length + palette2.length + palette3.length];
  255. System.arraycopy(palette1, 0, resultPalette, 0, palette1.length);
  256. System.arraycopy(palette2, 0, resultPalette, palette1.length, palette2.length);
  257. System.arraycopy(palette3,
  258. 0,
  259. resultPalette,
  260. palette1.length + palette2.length,
  261. palette1.length);
  262. return resultPalette;
  263. }
  264. @SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX")
  265. private static Color[] mixPalette(int steps, Color color1, Color color2) {
  266. Color[] palette = new Color[steps];
  267. palette[0] = color1;
  268. float[] step = stepCalc(steps, color1, color2);
  269. for (int i = 1; i < steps; i++) {
  270. int r = (int) (color1.r + step[0] * i);
  271. int g = (int) (color1.g + step[1] * i);
  272. int b = (int) (color1.b + step[2] * i);
  273. palette[i] = new Color(r, g, b);
  274. }
  275. return palette;
  276. }
  277. private static float[] stepCalc(int steps, Color color1, Color color2) {
  278. float[] step = new float[3];
  279. step[0] = (color2.r - color1.r) / (float) steps;
  280. step[1] = (color2.g - color1.g) / (float) steps;
  281. step[2] = (color2.b - color1.b) / (float) steps;
  282. return step;
  283. }
  284. public static class Color {
  285. public int a = 255;
  286. public int r;
  287. public int g;
  288. public int b;
  289. public Color(int r, int g, int b) {
  290. this.r = r;
  291. this.g = g;
  292. this.b = b;
  293. }
  294. public Color(int a, int r, int g, int b) {
  295. this.a = a;
  296. this.r = r;
  297. this.g = g;
  298. this.b = b;
  299. }
  300. @Override
  301. public boolean equals(@Nullable Object obj) {
  302. if (!(obj instanceof Color)) {
  303. return false;
  304. }
  305. Color other = (Color) obj;
  306. return this.r == other.r && this.g == other.g && this.b == other.b;
  307. }
  308. @Override
  309. public int hashCode() {
  310. return r * 10000 + g * 1000 + b;
  311. }
  312. }
  313. public static String md5(String string) throws NoSuchAlgorithmException {
  314. MessageDigest md5 = MessageDigest.getInstance("MD5");
  315. md5.update(string.getBytes(Charset.defaultCharset()));
  316. return new String(Hex.encodeHex(md5.digest()));
  317. }
  318. /**
  319. * Returns a new circular bitmap drawable by creating it from a bitmap, setting initial target density based on the
  320. * display metrics of the resources.
  321. *
  322. * @param resources the resources for initial target density
  323. * @param bitmap the original bitmap
  324. * @return the circular bitmap
  325. */
  326. @Nullable
  327. public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources,
  328. Bitmap bitmap,
  329. float radius) {
  330. if (bitmap == null) {
  331. return null;
  332. }
  333. RoundedBitmapDrawable roundedBitmap = RoundedBitmapDrawableFactory.create(resources, bitmap);
  334. roundedBitmap.setCircular(true);
  335. if (radius != -1) {
  336. roundedBitmap.setCornerRadius(radius);
  337. }
  338. return roundedBitmap;
  339. }
  340. @Nullable
  341. public static RoundedBitmapDrawable bitmapToCircularBitmapDrawable(Resources resources, Bitmap bitmap) {
  342. return bitmapToCircularBitmapDrawable(resources, bitmap, -1);
  343. }
  344. public static void setRoundedBitmap(Resources resources, Bitmap bitmap, float radius, ImageView imageView) {
  345. imageView.setImageDrawable(BitmapUtils.bitmapToCircularBitmapDrawable(resources,
  346. bitmap,
  347. radius));
  348. }
  349. public static Bitmap drawableToBitmap(Drawable drawable) {
  350. return drawableToBitmap(drawable, -1, -1);
  351. }
  352. @NonNull
  353. public static Bitmap drawableToBitmap(Drawable drawable, int desiredWidth, int desiredHeight) {
  354. if (drawable instanceof BitmapDrawable) {
  355. BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
  356. if (bitmapDrawable.getBitmap() != null) {
  357. return bitmapDrawable.getBitmap();
  358. }
  359. }
  360. Bitmap bitmap;
  361. int width;
  362. int height;
  363. if (desiredWidth > 0 && desiredHeight > 0) {
  364. width = desiredWidth;
  365. height = desiredHeight;
  366. } else {
  367. if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
  368. if (drawable.getBounds().width() > 0 && drawable.getBounds().height() > 0) {
  369. width = drawable.getBounds().width();
  370. height = drawable.getBounds().height();
  371. } else {
  372. width = 1;
  373. height = 1;
  374. }
  375. } else {
  376. width = drawable.getIntrinsicWidth();
  377. height = drawable.getIntrinsicHeight();
  378. }
  379. }
  380. bitmap = Bitmap.createBitmap(width,
  381. height,
  382. Bitmap.Config.ARGB_8888);
  383. Canvas canvas = new Canvas(bitmap);
  384. drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
  385. drawable.draw(canvas);
  386. return bitmap;
  387. }
  388. public static void setRoundedBitmap(Bitmap thumbnail, ImageView imageView) {
  389. BitmapUtils.setRoundedBitmap(getResources(),
  390. thumbnail,
  391. getResources().getDimension(R.dimen.file_icon_rounded_corner_radius),
  392. imageView);
  393. }
  394. public static void setRoundedBitmapForGridMode(Bitmap thumbnail, ImageView imageView) {
  395. BitmapUtils.setRoundedBitmap(getResources(),
  396. thumbnail,
  397. getResources().getDimension(R.dimen.file_icon_rounded_corner_radius_for_grid_mode),
  398. imageView);
  399. }
  400. public static Bitmap createAvatarWithStatus(Bitmap avatar, StatusType statusType, @NonNull String icon, Context context) {
  401. float avatarRadius = getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
  402. int width = DisplayUtils.convertDpToPixel(2 * avatarRadius, context);
  403. Bitmap output = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
  404. Canvas canvas = new Canvas(output);
  405. // avatar
  406. Bitmap croppedBitmap = getCroppedBitmap(avatar, width);
  407. canvas.drawBitmap(croppedBitmap, 0f, 0f, null);
  408. // status
  409. int statusSize = width / 4;
  410. Status status = new Status(statusType, "", icon, -1);
  411. StatusDrawable statusDrawable = new StatusDrawable(status, statusSize, context);
  412. canvas.translate(width / 2f, width / 2f);
  413. statusDrawable.draw(canvas);
  414. return output;
  415. }
  416. /**
  417. * Inspired from https://www.demo2s.com/android/android-bitmap-get-a-round-version-of-the-bitmap.html
  418. */
  419. public static Bitmap roundBitmap(Bitmap bitmap) {
  420. Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
  421. final Canvas canvas = new Canvas(output);
  422. final int color = R.color.white;
  423. final Paint paint = new Paint();
  424. final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
  425. final RectF rectF = new RectF(rect);
  426. paint.setAntiAlias(true);
  427. canvas.drawARGB(0, 0, 0, 0);
  428. paint.setColor(getResources().getColor(color, null));
  429. canvas.drawOval(rectF, paint);
  430. paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
  431. canvas.drawBitmap(bitmap, rect, rect, paint);
  432. return output;
  433. }
  434. /**
  435. * from https://stackoverflow.com/a/38249623
  436. **/
  437. public static Bitmap tintImage(Bitmap bitmap, int color) {
  438. Paint paint = new Paint();
  439. paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
  440. Bitmap bitmapResult = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
  441. Canvas canvas = new Canvas(bitmapResult);
  442. canvas.drawBitmap(bitmap, 0, 0, paint);
  443. return bitmapResult;
  444. }
  445. /**
  446. * from https://stackoverflow.com/a/12089127
  447. */
  448. private static Bitmap getCroppedBitmap(Bitmap bitmap, int width) {
  449. Bitmap output = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
  450. Canvas canvas = new Canvas(output);
  451. int color = -0xbdbdbe;
  452. Paint paint = new Paint();
  453. Rect rect = new Rect(0, 0, width, width);
  454. paint.setAntiAlias(true);
  455. canvas.drawARGB(0, 0, 0, 0);
  456. paint.setColor(color);
  457. canvas.drawCircle(width / 2f, width / 2f, width / 2f, paint);
  458. paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
  459. canvas.drawBitmap(Bitmap.createScaledBitmap(bitmap, width, width, false), rect, rect, paint);
  460. return output;
  461. }
  462. private static Resources getResources() {
  463. return MainApp.getAppContext().getResources();
  464. }
  465. }