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