1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.internal.util; 18 19 import android.content.ContentProviderClient; 20 import android.content.ContentResolver; 21 import android.graphics.Bitmap; 22 import android.graphics.Bitmap.Config; 23 import android.graphics.Canvas; 24 import android.graphics.ImageDecoder; 25 import android.graphics.ImageDecoder.ImageInfo; 26 import android.graphics.ImageDecoder.Source; 27 import android.graphics.Matrix; 28 import android.graphics.Paint; 29 import android.graphics.Point; 30 import android.graphics.PorterDuff; 31 import android.graphics.drawable.BitmapDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.util.Size; 36 37 import java.io.IOException; 38 39 /** 40 * Utility class for image analysis and processing. 41 * 42 * @hide 43 */ 44 public class ImageUtils { 45 46 // Amount (max is 255) that two channels can differ before the color is no longer "gray". 47 private static final int TOLERANCE = 20; 48 49 // Alpha amount for which values below are considered transparent. 50 private static final int ALPHA_TOLERANCE = 50; 51 52 // Size of the smaller bitmap we're actually going to scan. 53 private static final int COMPACT_BITMAP_SIZE = 64; // pixels 54 55 private int[] mTempBuffer; 56 private Bitmap mTempCompactBitmap; 57 private Canvas mTempCompactBitmapCanvas; 58 private Paint mTempCompactBitmapPaint; 59 private final Matrix mTempMatrix = new Matrix(); 60 61 /** 62 * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect 63 * gray". 64 * 65 * Instead of scanning every pixel in the bitmap, we first resize the bitmap to no more than 66 * COMPACT_BITMAP_SIZE^2 pixels using filtering. The hope is that any non-gray color elements 67 * will survive the squeezing process, contaminating the result with color. 68 */ isGrayscale(Bitmap bitmap)69 public boolean isGrayscale(Bitmap bitmap) { 70 int height = bitmap.getHeight(); 71 int width = bitmap.getWidth(); 72 73 // shrink to a more manageable (yet hopefully no more or less colorful) size 74 if (height > COMPACT_BITMAP_SIZE || width > COMPACT_BITMAP_SIZE) { 75 if (mTempCompactBitmap == null) { 76 mTempCompactBitmap = Bitmap.createBitmap( 77 COMPACT_BITMAP_SIZE, COMPACT_BITMAP_SIZE, Bitmap.Config.ARGB_8888 78 ); 79 mTempCompactBitmapCanvas = new Canvas(mTempCompactBitmap); 80 mTempCompactBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 81 mTempCompactBitmapPaint.setFilterBitmap(true); 82 } 83 mTempMatrix.reset(); 84 mTempMatrix.setScale( 85 (float) COMPACT_BITMAP_SIZE / width, 86 (float) COMPACT_BITMAP_SIZE / height, 87 0, 0); 88 mTempCompactBitmapCanvas.drawColor(0, PorterDuff.Mode.SRC); // select all, erase 89 mTempCompactBitmapCanvas.drawBitmap(bitmap, mTempMatrix, mTempCompactBitmapPaint); 90 bitmap = mTempCompactBitmap; 91 width = height = COMPACT_BITMAP_SIZE; 92 } 93 94 final int size = height * width; 95 ensureBufferSize(size); 96 bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height); 97 for (int i = 0; i < size; i++) { 98 if (!isGrayscale(mTempBuffer[i])) { 99 return false; 100 } 101 } 102 return true; 103 } 104 105 /** 106 * Makes sure that {@code mTempBuffer} has at least length {@code size}. 107 */ ensureBufferSize(int size)108 private void ensureBufferSize(int size) { 109 if (mTempBuffer == null || mTempBuffer.length < size) { 110 mTempBuffer = new int[size]; 111 } 112 } 113 114 /** 115 * Classifies a color as grayscale or not. Grayscale here means "very close to a perfect 116 * gray"; if all three channels are approximately equal, this will return true. 117 * 118 * Note that really transparent colors are always grayscale. 119 */ isGrayscale(int color)120 public static boolean isGrayscale(int color) { 121 int alpha = 0xFF & (color >> 24); 122 if (alpha < ALPHA_TOLERANCE) { 123 return true; 124 } 125 126 int r = 0xFF & (color >> 16); 127 int g = 0xFF & (color >> 8); 128 int b = 0xFF & color; 129 130 return Math.abs(r - g) < TOLERANCE 131 && Math.abs(r - b) < TOLERANCE 132 && Math.abs(g - b) < TOLERANCE; 133 } 134 135 /** 136 * Convert a drawable to a bitmap, scaled to fit within maxWidth and maxHeight. 137 */ buildScaledBitmap(Drawable drawable, int maxWidth, int maxHeight)138 public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth, 139 int maxHeight) { 140 if (drawable == null) { 141 return null; 142 } 143 int originalWidth = drawable.getIntrinsicWidth(); 144 int originalHeight = drawable.getIntrinsicHeight(); 145 146 if ((originalWidth <= maxWidth) && (originalHeight <= maxHeight) && 147 (drawable instanceof BitmapDrawable)) { 148 return ((BitmapDrawable) drawable).getBitmap(); 149 } 150 if (originalHeight <= 0 || originalWidth <= 0) { 151 return null; 152 } 153 154 // create a new bitmap, scaling down to fit the max dimensions of 155 // a large notification icon if necessary 156 float ratio = Math.min((float) maxWidth / (float) originalWidth, 157 (float) maxHeight / (float) originalHeight); 158 ratio = Math.min(1.0f, ratio); 159 int scaledWidth = (int) (ratio * originalWidth); 160 int scaledHeight = (int) (ratio * originalHeight); 161 Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888); 162 163 // and paint our app bitmap on it 164 Canvas canvas = new Canvas(result); 165 drawable.setBounds(0, 0, scaledWidth, scaledHeight); 166 drawable.draw(canvas); 167 168 return result; 169 } 170 171 /** 172 * @see https://developer.android.com/topic/performance/graphics/load-bitmap 173 */ calculateSampleSize(Size currentSize, Size requestedSize)174 public static int calculateSampleSize(Size currentSize, Size requestedSize) { 175 int inSampleSize = 1; 176 177 if (currentSize.getHeight() > requestedSize.getHeight() 178 || currentSize.getWidth() > requestedSize.getWidth()) { 179 final int halfHeight = currentSize.getHeight() / 2; 180 final int halfWidth = currentSize.getWidth() / 2; 181 182 // Calculate the largest inSampleSize value that is a power of 2 and keeps both 183 // height and width larger than the requested height and width. 184 while ((halfHeight / inSampleSize) >= requestedSize.getHeight() 185 && (halfWidth / inSampleSize) >= requestedSize.getWidth()) { 186 inSampleSize *= 2; 187 } 188 } 189 190 return inSampleSize; 191 } 192 193 /** 194 * Load a bitmap, and attempt to downscale to the required size, to save 195 * on memory. Updated to use newer and more compatible ImageDecoder. 196 * 197 * @see https://developer.android.com/topic/performance/graphics/load-bitmap 198 */ loadThumbnail(ContentResolver resolver, Uri uri, Size size)199 public static Bitmap loadThumbnail(ContentResolver resolver, Uri uri, Size size) 200 throws IOException { 201 202 try (ContentProviderClient client = resolver.acquireContentProviderClient(uri)) { 203 final Bundle opts = new Bundle(); 204 opts.putParcelable(ContentResolver.EXTRA_SIZE, Point.convert(size)); 205 206 return ImageDecoder.decodeBitmap(ImageDecoder.createSource(() -> { 207 return client.openTypedAssetFile(uri, "image/*", opts, null); 208 }), (ImageDecoder decoder, ImageInfo info, Source source) -> { 209 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 210 211 final int sample = calculateSampleSize(info.getSize(), size); 212 if (sample > 1) { 213 decoder.setTargetSampleSize(sample); 214 } 215 }); 216 } 217 } 218 } 219