1 /* 2 * Copyright (C) 2008 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 android.graphics.drawable.cts; 18 19 import android.content.Context; 20 import android.content.res.Configuration; 21 import android.content.res.Resources; 22 import android.content.res.XmlResourceParser; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.drawable.Drawable; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.util.Xml; 30 31 import androidx.annotation.IntegerRes; 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 35 import junit.framework.Assert; 36 37 import org.xmlpull.v1.XmlPullParser; 38 import org.xmlpull.v1.XmlPullParserException; 39 40 import java.io.File; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 44 /** 45 * The useful methods for graphics.drawable test. 46 */ 47 public class DrawableTestUtils { 48 private static final String LOGTAG = "DrawableTestUtils"; 49 50 // All of these constants range 0..1, with higher values being more lenient to differences 51 // between images. Values of zero mean no differences will be tolerated. 52 53 // Fail immediately if any *single* pixel diff exceeds this threshold 54 static final float FATAL_PIXEL_ERROR_THRESHOLD = 0.2f; 55 // Fail if the count of pixels with diffs above REGULAR_PIXEL_ERROR_THRESHOLD exceeds this ratio 56 static final float MAX_REGULAR_ERROR_RATIO = 0.05f; 57 // Threshold to count this pixel as a non-fatal error, the sum of which will be compared 58 // against MAX_REGULAR_ERROR_RATIO 59 static final float REGULAR_PIXEL_ERROR_THRESHOLD = 0.02f; 60 skipCurrentTag(XmlPullParser parser)61 public static void skipCurrentTag(XmlPullParser parser) 62 throws XmlPullParserException, IOException { 63 int outerDepth = parser.getDepth(); 64 int type; 65 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 66 && (type != XmlPullParser.END_TAG 67 || parser.getDepth() > outerDepth)) { 68 } 69 } 70 71 /** 72 * Retrieve an AttributeSet from a XML. 73 * 74 * @param parser the XmlPullParser to use for the xml parsing. 75 * @param searchedNodeName the name of the target node. 76 * @return the AttributeSet retrieved from specified node. 77 * @throws IOException 78 * @throws XmlPullParserException 79 */ getAttributeSet(XmlResourceParser parser, String searchedNodeName)80 public static AttributeSet getAttributeSet(XmlResourceParser parser, String searchedNodeName) 81 throws XmlPullParserException, IOException { 82 AttributeSet attrs = null; 83 int type; 84 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 85 && type != XmlPullParser.START_TAG) { 86 } 87 String nodeName = parser.getName(); 88 if (!"alias".equals(nodeName)) { 89 throw new RuntimeException(); 90 } 91 int outerDepth = parser.getDepth(); 92 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 93 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 94 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 95 continue; 96 } 97 nodeName = parser.getName(); 98 if (searchedNodeName.equals(nodeName)) { 99 outerDepth = parser.getDepth(); 100 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 101 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 102 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 103 continue; 104 } 105 nodeName = parser.getName(); 106 attrs = Xml.asAttributeSet(parser); 107 break; 108 } 109 break; 110 } else { 111 skipCurrentTag(parser); 112 } 113 } 114 return attrs; 115 } 116 getResourceParser(Resources res, int resId)117 public static XmlResourceParser getResourceParser(Resources res, int resId) 118 throws XmlPullParserException, IOException { 119 final XmlResourceParser parser = res.getXml(resId); 120 int type; 121 while ((type = parser.next()) != XmlPullParser.START_TAG 122 && type != XmlPullParser.END_DOCUMENT) { 123 // Empty loop 124 } 125 return parser; 126 } 127 setResourcesDensity(Resources res, int densityDpi)128 public static void setResourcesDensity(Resources res, int densityDpi) { 129 final Configuration config = new Configuration(); 130 config.setTo(res.getConfiguration()); 131 config.densityDpi = densityDpi; 132 res.updateConfiguration(config, null); 133 } 134 135 /** 136 * Implements scaling as used by the Bitmap class. Resulting values are 137 * rounded up (as distinct from resource scaling, which truncates or rounds 138 * to the nearest pixel). 139 * 140 * @param size the pixel size to scale 141 * @param sdensity the source density that corresponds to the size 142 * @param tdensity the target density 143 * @return the pixel size scaled for the target density 144 */ scaleBitmapFromDensity(int size, int sdensity, int tdensity)145 public static int scaleBitmapFromDensity(int size, int sdensity, int tdensity) { 146 if (sdensity == 0 || tdensity == 0 || sdensity == tdensity) { 147 return size; 148 } 149 150 // Scale by tdensity / sdensity, rounding up. 151 return ((size * tdensity) + (sdensity >> 1)) / sdensity; 152 } 153 154 /** 155 * Asserts that two images are similar within the given thresholds. 156 * 157 * @param message Error message 158 * @param expected Expected bitmap 159 * @param actual Actual bitmap 160 * @param fatalPixelErrorThreshold 0..1 - Fails immediately if any *single* pixel diff exceeds 161 * this threshold 162 * @param maxRegularErrorRatio 0..1 - Fails if the count of pixels with diffs above 163 * regularPixelErrorThreshold exceeds this ratio 164 * @param regularPixelErrorThreshold 0..1 - Threshold to count this pixel as a non-fatal error, 165 * the sum of which will be compared against MAX_REGULAR_ERROR_RATIO 166 */ compareImages( String message, Bitmap expected, Bitmap actual, float fatalPixelErrorThreshold, float maxRegularErrorRatio, float regularPixelErrorThreshold)167 public static void compareImages( 168 String message, 169 Bitmap expected, 170 Bitmap actual, 171 float fatalPixelErrorThreshold, 172 float maxRegularErrorRatio, 173 float regularPixelErrorThreshold) { 174 int idealWidth = expected.getWidth(); 175 int idealHeight = expected.getHeight(); 176 177 Assert.assertTrue(idealWidth == actual.getWidth()); 178 Assert.assertTrue(idealHeight == actual.getHeight()); 179 180 int totalDiffPixelCount = 0; 181 float totalPixelCount = idealWidth * idealHeight; 182 for (int x = 0; x < idealWidth; x++) { 183 for (int y = 0; y < idealHeight; y++) { 184 int idealColor = expected.getPixel(x, y); 185 int givenColor = actual.getPixel(x, y); 186 if (idealColor == givenColor) 187 continue; 188 if (Color.alpha(idealColor) + Color.alpha(givenColor) == 0) { 189 continue; 190 } 191 192 float idealAlpha = Color.alpha(idealColor) / 255.0f; 193 float givenAlpha = Color.alpha(givenColor) / 255.0f; 194 195 // compare premultiplied color values 196 float pixelError = 0; 197 pixelError += Math.abs((idealAlpha * Color.red(idealColor)) 198 - (givenAlpha * Color.red(givenColor))); 199 pixelError += Math.abs((idealAlpha * Color.green(idealColor)) 200 - (givenAlpha * Color.green(givenColor))); 201 pixelError += Math.abs((idealAlpha * Color.blue(idealColor)) 202 - (givenAlpha * Color.blue(givenColor))); 203 pixelError += Math.abs(Color.alpha(idealColor) - Color.alpha(givenColor)); 204 pixelError /= 1024.0f; 205 206 if (pixelError > fatalPixelErrorThreshold) { 207 Assert.fail( 208 String.format( 209 "%s: pixelError of %f exceeds fatalPixelErrorThreshold of %f" 210 + " for pixel (%d, %d)", 211 message, 212 pixelError, 213 fatalPixelErrorThreshold, 214 x, 215 y)); 216 } 217 218 if (pixelError > regularPixelErrorThreshold) { 219 totalDiffPixelCount++; 220 } 221 } 222 } 223 float countedErrorRatio = totalDiffPixelCount / totalPixelCount; 224 if (countedErrorRatio > maxRegularErrorRatio) { 225 Assert.fail( 226 String.format( 227 "%s: countedErrorRatio of %f exceeds maxRegularErrorRatio of %f for" 228 + " %dx%d image", 229 message, 230 countedErrorRatio, 231 maxRegularErrorRatio, 232 idealWidth, 233 idealHeight)); 234 } 235 } 236 237 /** 238 * Returns the {@link Color} at the specified location in the {@link Drawable}. 239 */ getPixel(Drawable d, int x, int y)240 public static int getPixel(Drawable d, int x, int y) { 241 final int w = Math.max(d.getIntrinsicWidth(), x + 1); 242 final int h = Math.max(d.getIntrinsicHeight(), y + 1); 243 final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 244 final Canvas c = new Canvas(b); 245 d.setBounds(0, 0, w, h); 246 d.draw(c); 247 248 final int pixel = b.getPixel(x, y); 249 b.recycle(); 250 return pixel; 251 } 252 253 /** 254 * Save a bitmap for debugging or golden image (re)generation purpose. 255 * The file name will be referred from the resource id, plus optionally {@code extras}, and 256 * "_golden" 257 */ saveAutoNamedVectorDrawableIntoPNG(@onNull Context context, @NonNull Bitmap bitmap, @IntegerRes int resId, @Nullable String extras)258 static void saveAutoNamedVectorDrawableIntoPNG(@NonNull Context context, @NonNull Bitmap bitmap, 259 @IntegerRes int resId, @Nullable String extras) 260 throws IOException { 261 String originalFilePath = context.getResources().getString(resId); 262 File originalFile = new File(originalFilePath); 263 String fileFullName = originalFile.getName(); 264 String fileTitle = fileFullName.substring(0, fileFullName.lastIndexOf(".")); 265 String outputFolder = context.getExternalFilesDir(null).getAbsolutePath(); 266 if (extras != null) { 267 fileTitle += "_" + extras; 268 } 269 saveVectorDrawableIntoPNG(bitmap, outputFolder, fileTitle); 270 } 271 272 /** 273 * Save a {@code bitmap} to the {@code fileFullName} plus "_golden". 274 */ saveVectorDrawableIntoPNG(@onNull Bitmap bitmap, @NonNull String outputFolder, @NonNull String fileFullName)275 static void saveVectorDrawableIntoPNG(@NonNull Bitmap bitmap, @NonNull String outputFolder, 276 @NonNull String fileFullName) 277 throws IOException { 278 // Save the image to the disk. 279 FileOutputStream out = null; 280 try { 281 File folder = new File(outputFolder); 282 if (!folder.exists()) { 283 folder.mkdir(); 284 } 285 String outputFilename = outputFolder + "/" + fileFullName + "_golden"; 286 outputFilename +=".png"; 287 File outputFile = new File(outputFilename); 288 if (!outputFile.exists()) { 289 outputFile.createNewFile(); 290 } 291 292 out = new FileOutputStream(outputFile, false); 293 bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); 294 Log.v(LOGTAG, "Write test No." + outputFilename + " to file successfully."); 295 } catch (Exception e) { 296 e.printStackTrace(); 297 } finally { 298 if (out != null) { 299 out.close(); 300 } 301 } 302 } 303 } 304