/* Copyright (C) 2017-2024 Arjan Schrijver, Frank Slezak, HelloCodeberg, José Rebelo This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.util; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androidx.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.ByteOrder; public class BitmapUtil { private static final Logger LOG = LoggerFactory.getLogger(BitmapUtil.class); /** * Downscale a bitmap to a maximum resolution. Doesn't scale if the bitmap is already smaller than the max resolution. * * @param bitmap * @param maxWidth * @param maxHeight * @return */ public static Bitmap scaleWithMax(Bitmap bitmap, int maxWidth, int maxHeight) { // Scale image only if necessary if ((bitmap.getWidth() > maxWidth) || (bitmap.getHeight() > maxHeight)) { bitmap = Bitmap.createScaledBitmap(bitmap, maxWidth, maxHeight, true); } return bitmap; } /** * Get a Bitmap from any given Drawable. * * Note that this code will fail if the drawable is 0x0. * * @param drawable A Drawable to convert. * @return A Bitmap representing the drawable. */ public static Bitmap convertDrawableToBitmap(Drawable drawable) { // If whoever made this drawable decided to be nice to us... if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } /** * Converts the provided Bitmap to grayscale. * * @param bitmap * @return */ public static Bitmap convertToGrayscale(Bitmap bitmap) { Canvas c = new Canvas(bitmap); Paint paint = new Paint(); ColorMatrix cm = new ColorMatrix(); cm.setSaturation(0); ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm); paint.setColorFilter(f); c.drawBitmap(bitmap, 0, 0, paint); return bitmap; } /** * Change the contrast and brightness on a Bitmap * * Code from: https://stackoverflow.com/questions/12891520/how-to-programmatically-change-contrast-of-a-bitmap-in-android#17887577 * * @param bmp input bitmap * @param contrast 0..10 1 is default * @param brightness -255..255 0 is default * @return new bitmap */ public static Bitmap changeBitmapContrastBrightness(Bitmap bmp, float contrast, float brightness) { ColorMatrix cm = new ColorMatrix(new float[] { contrast, 0, 0, 0, brightness, 0, contrast, 0, 0, brightness, 0, 0, contrast, 0, brightness, 0, 0, 0, 1, 0 }); Bitmap ret = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), bmp.getConfig()); Canvas canvas = new Canvas(ret); Paint paint = new Paint(); paint.setColorFilter(new ColorMatrixColorFilter(cm)); canvas.drawBitmap(bmp, 0, 0, paint); return ret; } /** * Invert the colors of a Bitmap * * @param bmp input bitmap * @return new bitmap */ public static Bitmap invertBitmapColors(Bitmap bmp) { ColorMatrix colorMatrix_Inverted = new ColorMatrix(new float[] { -1, 0, 0, 0, 255, 0, -1, 0, 0, 255, 0, 0, -1, 0, 255, 0, 0, 0, 1, 0}); Bitmap ret = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), bmp.getConfig()); Canvas canvas = new Canvas(ret); Paint paint = new Paint(); paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix_Inverted)); canvas.drawBitmap(bmp, 0, 0, paint); return ret; } /** * Crops a circular image from the center of the provided Bitmap. * From: https://www.tutorialspoint.com/android-how-to-crop-circular-area-from-bitmap * @param srcBitmap * @return */ public static Bitmap getCircularBitmap(Bitmap srcBitmap) { // Calculate the circular bitmap width with border int squareBitmapWidth = Math.min(srcBitmap.getWidth(), srcBitmap.getHeight()); // Initialize a new instance of Bitmap Bitmap dstBitmap = Bitmap.createBitmap ( squareBitmapWidth, // Width squareBitmapWidth, // Height Bitmap.Config.ARGB_8888 // Config ); Canvas canvas = new Canvas(dstBitmap); // Initialize a new Paint instance Paint paint = new Paint(); paint.setAntiAlias(true); Rect rect = new Rect(0, 0, squareBitmapWidth, squareBitmapWidth); RectF rectF = new RectF(rect); canvas.drawOval(rectF, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); // Calculate the left and top of copied bitmap float left = (squareBitmapWidth-srcBitmap.getWidth())/2; float top = (squareBitmapWidth-srcBitmap.getHeight())/2; canvas.drawBitmap(srcBitmap, left, top, paint); // Return the circular bitmap return dstBitmap; } /** * Rotates a given Bitmap * @param bitmap input bitmap * @param degree int Degree of rotation * @return new bitmap */ public static Bitmap rotateImage(Bitmap bitmap, int degree) { Matrix matrix = new Matrix(); matrix.postRotate(degree); return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); } /** * Overlays two bitmaps on top of each other, * bmp1 is assumed to be larger or equal to bmp2 * From: https://stackoverflow.com/a/2287218 * @param bmp1 * @param bmp2 * @return new Bitmap */ public static Bitmap overlay(Bitmap bmp1, Bitmap bmp2) { Bitmap bmOverlay = Bitmap.createBitmap(bmp1.getWidth(), bmp1.getHeight(), bmp1.getConfig()); Canvas canvas = new Canvas(bmOverlay); canvas.drawBitmap(bmp1, new Matrix(), null); canvas.drawBitmap(bmp2, new Matrix(), null); return bmOverlay; } /** * Converts a {@link Drawable} to a {@link Bitmap}, in ARGB8888 mode. * * @param drawable the {@link Drawable} * @return the {@link Bitmap} */ public static Bitmap toBitmap(final Drawable drawable) { if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } /** * Converts a {@link Bitmap} to an uncompressed TGA image, as raw bytes, in RGB565 encoding. * @param bmp the {@link Bitmap} to convert. * @param width the target width * @param height the target height * @param id the TGA ID * @return the raw bytes for the TGA image */ public static byte[] convertToTgaRGB565(final Bitmap bmp, final int width, final int height, final byte[] id) { final Bitmap bmp565; if (bmp.getConfig().equals(Bitmap.Config.RGB_565) && bmp.getWidth() == width && bmp.getHeight() == height) { // Right encoding and size bmp565 = bmp; } else { // Convert encoding / scale bmp565 = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); final Canvas canvas = new Canvas(bmp565); final Rect rect = new Rect(0, 0, width, height); canvas.drawBitmap(bmp, null, rect, null); } int size = bmp565.getRowBytes() * bmp565.getHeight(); final ByteBuffer bmp565buf = ByteBuffer.allocate(size); bmp565.copyPixelsToBuffer(bmp565buf); // As per https://en.wikipedia.org/wiki/Truevision_TGA // 18 bytes final byte[] header = { // ID length (byte) id.length, // Color map type - (0 - no color map) 0x00, // Image type (2 - uncompressed true-color image) 0x02, // Color map specification (5 bytes) 0x00, 0x00, // first entry index 0x00, 0x00, /// color map length 0x00, // color map entry size // Image dimensions and format (10 bytes) 0x00, 0x00, // x origin 0x00, 0x00, // y origin (byte) (width & 0xff), (byte) ((width >> 8) & 0xff), // width (byte) (height & 0xff), (byte) ((height >> 8) & 0xff), // height 16, // bits per pixel (10) 0x20, // image descriptor (0x20, 00100000) // bits 3-0 give the alpha channel depth, bits 5-4 give pixel ordering // Bit 4 of the image descriptor byte indicates right-to-left pixel ordering if set. // Bit 5 indicates an ordering of top-to-bottom. Otherwise, pixels are stored in bottom-to-top, left-to-right order. }; final ByteBuffer tga565buf = ByteBuffer.allocate(header.length + id.length + size); tga565buf.put(header); tga565buf.put(id); tga565buf.put(bmp565buf.array()); return tga565buf.array(); } public static boolean isPng(final byte[] data) { return ArrayUtils.equals(data, new byte[] {(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}, 0); } @Nullable public static Bitmap decodeTga(final byte[] bytes) { final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); final byte idLength = buf.get(); final byte colorMapType = buf.get(); final byte imageType = buf.get(); final short colorMapFirstEntryIndex = buf.getShort(); final short colorMapLength = buf.getShort(); final byte colorMapEntrySize = buf.get(); final short xOrigin = buf.getShort(); final short yOrigin = buf.getShort(); final short width = buf.getShort(); final short height = buf.getShort(); final byte bitsPerPixel = buf.get(); final byte imageDescriptor = buf.get(); final byte[] id = new byte[idLength]; buf.get(id); if (imageType== 2 && colorMapType == 0) { // Parse true-color uncompressed image data as RGB565 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); bitmap.copyPixelsFromBuffer(buf); return bitmap; } if (colorMapType != 0x01) { LOG.warn("Unknown color map type {}", colorMapType); return null; } if (colorMapEntrySize != 32) { LOG.warn("Color map entry size {} not supported", colorMapEntrySize); return null; } final int[] colorMap = new int[colorMapLength]; for (int i = 0; i < colorMapLength; i++) { colorMap[i] = buf.getInt(); } final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); switch (imageType) { case 1: if (bitsPerPixel != 8) { LOG.warn("Unsupported bits per pixel {} for imageType 1", bitsPerPixel); return null; } // uncompressed color-mapped image for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { bitmap.setPixel(x, y, colorMap[buf.get() & 0xff]); } } break; case 9: // run-length encoded color-mapped image int i = 0; while (buf.position() < buf.limit()) { final byte b = buf.get(); final int count = (b & 127) + 1; if ((b & 128) != 0) { // msb 1 - run-length encoded final int val = buf.get() & 0xff; for (int j = 0; j < count; j++) { int y = i / width; int x = i % width; bitmap.setPixel(x, y, colorMap[val]); i++; } } else { // msb 0 - raw pixels for (int j = 0; j < count; j++) { int y = i / width; int x = i % width; bitmap.setPixel(x, y, colorMap[buf.get() & 0xff]); i++; } } } break; default: LOG.warn("Image type {} not supported", imageType); return null; } final int remainingBytes = buf.limit() - buf.position(); if (remainingBytes != 0) { LOG.warn("There are {} bytes remaining in the buffer", remainingBytes); } return bitmap; } }