BitmapUtils.java 20 KB

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