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