1 /*
2  * Copyright (C) 2016 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.compatibility.common.util;
18 
19 import static org.junit.Assert.assertTrue;
20 
21 import android.app.WallpaperManager;
22 import android.content.Context;
23 import android.graphics.Bitmap;
24 import android.graphics.Bitmap.CompressFormat;
25 import android.graphics.Color;
26 import android.util.Log;
27 
28 import java.io.ByteArrayInputStream;
29 import java.io.ByteArrayOutputStream;
30 import java.io.File;
31 import java.io.FileOutputStream;
32 import java.lang.reflect.Method;
33 import java.util.Random;
34 
35 public class BitmapUtils {
36     private static final String TAG = "BitmapUtils";
37 
BitmapUtils()38     private BitmapUtils() {}
39 
compareBasicBitmapsInfo(Bitmap bmp1, Bitmap bmp2)40     private static Boolean compareBasicBitmapsInfo(Bitmap bmp1, Bitmap bmp2) {
41         if (bmp1 == bmp2) {
42             return Boolean.TRUE;
43         }
44 
45         if (bmp1 == null) {
46             Log.d(TAG, "compareBitmaps() failed because bmp1 is null");
47             return Boolean.FALSE;
48         }
49 
50         if (bmp2 == null) {
51             Log.d(TAG, "compareBitmaps() failed because bmp2 is null");
52             return Boolean.FALSE;
53         }
54 
55         if ((bmp1.getWidth() != bmp2.getWidth()) || (bmp1.getHeight() != bmp2.getHeight())) {
56             Log.d(TAG, "compareBitmaps() failed because sizes don't match "
57                     + "bmp1=(" + bmp1.getWidth() + "x" + bmp1.getHeight() + "), "
58                     + "bmp2=(" + bmp2.getWidth() + "x" + bmp2.getHeight() + ")");
59             return Boolean.FALSE;
60         }
61 
62         if (bmp1.getConfig() != bmp2.getConfig()) {
63             Log.d(TAG, "compareBitmaps() failed because configs don't match "
64                     + "bmp1=(" + bmp1.getConfig() + "), "
65                     + "bmp2=(" + bmp2.getConfig() + ")");
66             return Boolean.FALSE;
67         }
68 
69         return null;
70     }
71 
72     /**
73      * Compares two bitmaps by pixels.
74      */
compareBitmaps(Bitmap bmp1, Bitmap bmp2)75     public static boolean compareBitmaps(Bitmap bmp1, Bitmap bmp2) {
76         final Boolean basicComparison = compareBasicBitmapsInfo(bmp1, bmp2);
77         if (basicComparison != null) return basicComparison.booleanValue();
78 
79         for (int i = 0; i < bmp1.getWidth(); i++) {
80             for (int j = 0; j < bmp1.getHeight(); j++) {
81                 if (bmp1.getPixel(i, j) != bmp2.getPixel(i, j)) {
82                     Log.d(TAG, "compareBitmaps(): pixels (" + i + ", " + j + ") don't match");
83                     return false;
84                 }
85             }
86         }
87         return true;
88     }
89 
90     /**
91      *  Compares two bitmaps by pixels, with a buffer for mistmatches.
92      *
93      *  <p>For example, if {@code minimumPrecision} is 0.99, at least 99% of the pixels should
94      *  match.
95      */
compareBitmaps(Bitmap bmp1, Bitmap bmp2, double minimumPrecision)96     public static boolean compareBitmaps(Bitmap bmp1, Bitmap bmp2, double minimumPrecision) {
97         final Boolean basicComparison = compareBasicBitmapsInfo(bmp1, bmp2);
98         if (basicComparison != null) return basicComparison.booleanValue();
99 
100         final int width = bmp1.getWidth();
101         final int height = bmp1.getHeight();
102 
103         final long numberPixels = width * height;
104         long numberMismatches = 0;
105 
106         for (int i = 0; i < width; i++) {
107             for (int j = 0; j < height; j++) {
108                 if (bmp1.getPixel(i, j) != bmp2.getPixel(i, j)) {
109                     numberMismatches++;
110                     if (numberMismatches <= 10) {
111                         // Let's not spam logcat...
112                         Log.w(TAG, "compareBitmaps(): pixels (" + i + ", " + j + ") don't match");
113                     }
114                 }
115             }
116         }
117         final double actualPrecision = ((double) numberPixels - numberMismatches) / (numberPixels);
118         Log.v(TAG, "compareBitmaps(): numberPixels=" + numberPixels
119                 + ", numberMismatches=" + numberMismatches
120                 + ", minimumPrecision=" + minimumPrecision
121                 + ", actualPrecision=" + actualPrecision);
122         return actualPrecision >= minimumPrecision;
123     }
124 
generateRandomBitmap(int width, int height)125     public static Bitmap generateRandomBitmap(int width, int height) {
126         final Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
127         final Random generator = new Random();
128         for (int x = 0; x < width; x++) {
129             for (int y = 0; y < height; y++) {
130                 bmp.setPixel(x, y, generator.nextInt(Integer.MAX_VALUE));
131             }
132         }
133         return bmp;
134     }
135 
generateWhiteBitmap(int width, int height)136     public static Bitmap generateWhiteBitmap(int width, int height) {
137         final Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
138         bmp.eraseColor(Color.WHITE);
139         return bmp;
140     }
141 
getWallpaperBitmap(Context context)142     public static Bitmap getWallpaperBitmap(Context context) throws Exception {
143         WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
144         Class<?> noparams[] = {};
145         Class<?> wmClass = wallpaperManager.getClass();
146         Method methodGetBitmap = wmClass.getDeclaredMethod("getBitmap", noparams);
147         return (Bitmap) methodGetBitmap.invoke(wallpaperManager, null);
148     }
149 
bitmapToInputStream(Bitmap bmp)150     public static ByteArrayInputStream bitmapToInputStream(Bitmap bmp) {
151         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
152         bmp.compress(CompressFormat.PNG, 0 /*ignored for PNG*/, bos);
153         byte[] bitmapData = bos.toByteArray();
154         return new ByteArrayInputStream(bitmapData);
155     }
156 
logIfBitmapSolidColor(String fileName, Bitmap bitmap)157     private static void logIfBitmapSolidColor(String fileName, Bitmap bitmap) {
158         int firstColor = bitmap.getPixel(0, 0);
159         for (int x = 0; x < bitmap.getWidth(); x++) {
160             for (int y = 0; y < bitmap.getHeight(); y++) {
161                 if (bitmap.getPixel(x, y) != firstColor) {
162                     return;
163                 }
164             }
165         }
166 
167         Log.w(TAG, String.format("%s entire bitmap color is %x", fileName, firstColor));
168     }
169 
saveBitmap(Bitmap bitmap, String directoryName, String fileName)170     public static void saveBitmap(Bitmap bitmap, String directoryName, String fileName) {
171         new File(directoryName).mkdirs(); // create dirs if needed
172 
173         Log.d(TAG, "Saving file: " + fileName + " in directory: " + directoryName);
174 
175         if (bitmap == null) {
176             Log.d(TAG, "File not saved, bitmap was null");
177             return;
178         }
179 
180         logIfBitmapSolidColor(fileName, bitmap);
181 
182         File file = new File(directoryName, fileName);
183         try (FileOutputStream fileStream = new FileOutputStream(file)) {
184             bitmap.compress(Bitmap.CompressFormat.PNG, 0 /* ignored for PNG */, fileStream);
185             fileStream.flush();
186         } catch (Exception e) {
187             e.printStackTrace();
188         }
189     }
190 
191     // Compare expected to actual to see if their diff is less than mseMargin.
192     // lessThanMargin is to indicate whether we expect the diff to be
193     // "less than" or "no less than".
compareBitmapsMse(Bitmap expected, Bitmap actual, int mseMargin, boolean lessThanMargin, boolean isPremultiplied)194     public static boolean compareBitmapsMse(Bitmap expected, Bitmap actual,
195             int mseMargin, boolean lessThanMargin, boolean isPremultiplied) {
196         final Boolean basicComparison = compareBasicBitmapsInfo(expected, actual);
197         if (basicComparison != null) return basicComparison.booleanValue();
198 
199         double mse = 0;
200         int width = expected.getWidth();
201         int height = expected.getHeight();
202 
203         // Bitmap.getPixels() returns colors with non-premultiplied ARGB values.
204         int[] expColors = new int [width * height];
205         expected.getPixels(expColors, 0, width, 0, 0, width, height);
206 
207         int[] actualColors = new int [width * height];
208         actual.getPixels(actualColors, 0, width, 0, 0, width, height);
209 
210         for (int row = 0; row < height; ++row) {
211             for (int col = 0; col < width; ++col) {
212                 int idx = row * width + col;
213                 mse += distance(expColors[idx], actualColors[idx], isPremultiplied);
214             }
215         }
216         mse /= width * height;
217 
218         Log.i(TAG, "MSE: " + mse);
219         if (lessThanMargin) {
220             if (mse > mseMargin) {
221                 Log.d(TAG, "MSE too large for normal case: " + mse);
222                 return false;
223             }
224             return true;
225         } else {
226             if (mse <= mseMargin) {
227                 Log.d(TAG, "MSE too small for abnormal case: " + mse);
228                 return false;
229             }
230             return true;
231         }
232     }
233 
234     // Same as above, but asserts compareBitmapsMse's return value.
assertBitmapsMse(Bitmap expected, Bitmap actual, int mseMargin, boolean lessThanMargin, boolean isPremultiplied)235     public static void assertBitmapsMse(Bitmap expected, Bitmap actual,
236             int mseMargin, boolean lessThanMargin, boolean isPremultiplied) {
237         assertTrue(compareBitmapsMse(expected, actual, mseMargin, lessThanMargin, isPremultiplied));
238     }
239 
multiplyAlpha(int color, int alpha)240     private static int multiplyAlpha(int color, int alpha) {
241         return (color * alpha + 127) / 255;
242     }
243 
244     // For the Bitmap with Alpha, multiply the Alpha values to get the effective
245     // RGB colors and then compute the color-distance.
distance(int expect, int actual, boolean isPremultiplied)246     private static double distance(int expect, int actual, boolean isPremultiplied) {
247         if (isPremultiplied) {
248             final int a1 = Color.alpha(actual);
249             final int a2 = Color.alpha(expect);
250             final int r = multiplyAlpha(Color.red(actual), a1) -
251                     multiplyAlpha(Color.red(expect), a2);
252             final int g = multiplyAlpha(Color.green(actual), a1) -
253                     multiplyAlpha(Color.green(expect), a2);
254             final int b = multiplyAlpha(Color.blue(actual), a1) -
255                     multiplyAlpha(Color.blue(expect), a2);
256             return r * r + g * g + b * b;
257         } else {
258             int r = Color.red(actual) - Color.red(expect);
259             int g = Color.green(actual) - Color.green(expect);
260             int b = Color.blue(actual) - Color.blue(expect);
261             return r * r + g * g + b * b;
262         }
263     }
264 }
265