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 return buildScaledBitmap(drawable, maxWidth, maxHeight, false); 141 } 142 143 /** 144 * Convert a drawable to a bitmap, scaled to fit within maxWidth and maxHeight. 145 * 146 * @param allowUpscaling if true, the drawable will not only be scaled down, but also scaled up 147 * to fit within the maximum size given. This is useful for converting 148 * vectorized icons which usually have a very small intrinsic size. 149 */ buildScaledBitmap(Drawable drawable, int maxWidth, int maxHeight, boolean allowUpscaling)150 public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth, 151 int maxHeight, boolean allowUpscaling) { 152 if (drawable == null) { 153 return null; 154 } 155 int originalWidth = drawable.getIntrinsicWidth(); 156 int originalHeight = drawable.getIntrinsicHeight(); 157 158 if ((originalWidth <= maxWidth) && (originalHeight <= maxHeight) && 159 (drawable instanceof BitmapDrawable)) { 160 return ((BitmapDrawable) drawable).getBitmap(); 161 } 162 if (originalHeight <= 0 || originalWidth <= 0) { 163 return null; 164 } 165 166 // create a new bitmap, scaling down to fit the max dimensions of 167 // a large notification icon if necessary 168 float ratio = Math.min((float) maxWidth / (float) originalWidth, 169 (float) maxHeight / (float) originalHeight); 170 if (!allowUpscaling) { 171 ratio = Math.min(1.0f, ratio); 172 } 173 int scaledWidth = (int) (ratio * originalWidth); 174 int scaledHeight = (int) (ratio * originalHeight); 175 Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888); 176 177 // and paint our app bitmap on it 178 Canvas canvas = new Canvas(result); 179 drawable.setBounds(0, 0, scaledWidth, scaledHeight); 180 drawable.draw(canvas); 181 182 return result; 183 } 184 185 /** 186 * @see https://developer.android.com/topic/performance/graphics/load-bitmap 187 */ calculateSampleSize(Size currentSize, Size requestedSize)188 public static int calculateSampleSize(Size currentSize, Size requestedSize) { 189 int inSampleSize = 1; 190 191 if (currentSize.getHeight() > requestedSize.getHeight() 192 || currentSize.getWidth() > requestedSize.getWidth()) { 193 final int halfHeight = currentSize.getHeight() / 2; 194 final int halfWidth = currentSize.getWidth() / 2; 195 196 // Calculate the largest inSampleSize value that is a power of 2 and keeps both 197 // height and width larger than the requested height and width. 198 while ((halfHeight / inSampleSize) >= requestedSize.getHeight() 199 && (halfWidth / inSampleSize) >= requestedSize.getWidth()) { 200 inSampleSize *= 2; 201 } 202 } 203 204 return inSampleSize; 205 } 206 207 /** 208 * Load a bitmap, and attempt to downscale to the required size, to save 209 * on memory. Updated to use newer and more compatible ImageDecoder. 210 * 211 * @see https://developer.android.com/topic/performance/graphics/load-bitmap 212 */ loadThumbnail(ContentResolver resolver, Uri uri, Size size)213 public static Bitmap loadThumbnail(ContentResolver resolver, Uri uri, Size size) 214 throws IOException { 215 216 try (ContentProviderClient client = resolver.acquireContentProviderClient(uri)) { 217 final Bundle opts = new Bundle(); 218 opts.putParcelable(ContentResolver.EXTRA_SIZE, 219 new Point(size.getWidth(), size.getHeight())); 220 221 return ImageDecoder.decodeBitmap(ImageDecoder.createSource(() -> { 222 return client.openTypedAssetFile(uri, "image/*", opts, null); 223 }), (ImageDecoder decoder, ImageInfo info, Source source) -> { 224 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 225 226 final int sample = calculateSampleSize(info.getSize(), size); 227 if (sample > 1) { 228 decoder.setTargetSampleSize(sample); 229 } 230 }); 231 } 232 } 233 } 234