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 // A small value is actually making sure that the values are matching 50 // exactly with the golden image. 51 // We can increase the threshold if the Skia is drawing with some variance 52 // on different devices. So far, the tests show they are matching correctly. 53 static final float PIXEL_ERROR_THRESHOLD = 0.03f; 54 static final float PIXEL_ERROR_COUNT_THRESHOLD = 0.005f; 55 static final int PIXEL_ERROR_TOLERANCE = 3; 56 skipCurrentTag(XmlPullParser parser)57 public static void skipCurrentTag(XmlPullParser parser) 58 throws XmlPullParserException, IOException { 59 int outerDepth = parser.getDepth(); 60 int type; 61 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 62 && (type != XmlPullParser.END_TAG 63 || parser.getDepth() > outerDepth)) { 64 } 65 } 66 67 /** 68 * Retrieve an AttributeSet from a XML. 69 * 70 * @param parser the XmlPullParser to use for the xml parsing. 71 * @param searchedNodeName the name of the target node. 72 * @return the AttributeSet retrieved from specified node. 73 * @throws IOException 74 * @throws XmlPullParserException 75 */ getAttributeSet(XmlResourceParser parser, String searchedNodeName)76 public static AttributeSet getAttributeSet(XmlResourceParser parser, String searchedNodeName) 77 throws XmlPullParserException, IOException { 78 AttributeSet attrs = null; 79 int type; 80 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 81 && type != XmlPullParser.START_TAG) { 82 } 83 String nodeName = parser.getName(); 84 if (!"alias".equals(nodeName)) { 85 throw new RuntimeException(); 86 } 87 int outerDepth = parser.getDepth(); 88 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 89 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 90 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 91 continue; 92 } 93 nodeName = parser.getName(); 94 if (searchedNodeName.equals(nodeName)) { 95 outerDepth = parser.getDepth(); 96 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 97 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 98 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 99 continue; 100 } 101 nodeName = parser.getName(); 102 attrs = Xml.asAttributeSet(parser); 103 break; 104 } 105 break; 106 } else { 107 skipCurrentTag(parser); 108 } 109 } 110 return attrs; 111 } 112 getResourceParser(Resources res, int resId)113 public static XmlResourceParser getResourceParser(Resources res, int resId) 114 throws XmlPullParserException, IOException { 115 final XmlResourceParser parser = res.getXml(resId); 116 int type; 117 while ((type = parser.next()) != XmlPullParser.START_TAG 118 && type != XmlPullParser.END_DOCUMENT) { 119 // Empty loop 120 } 121 return parser; 122 } 123 setResourcesDensity(Resources res, int densityDpi)124 public static void setResourcesDensity(Resources res, int densityDpi) { 125 final Configuration config = new Configuration(); 126 config.setTo(res.getConfiguration()); 127 config.densityDpi = densityDpi; 128 res.updateConfiguration(config, null); 129 } 130 131 /** 132 * Implements scaling as used by the Bitmap class. Resulting values are 133 * rounded up (as distinct from resource scaling, which truncates or rounds 134 * to the nearest pixel). 135 * 136 * @param size the pixel size to scale 137 * @param sdensity the source density that corresponds to the size 138 * @param tdensity the target density 139 * @return the pixel size scaled for the target density 140 */ scaleBitmapFromDensity(int size, int sdensity, int tdensity)141 public static int scaleBitmapFromDensity(int size, int sdensity, int tdensity) { 142 if (sdensity == 0 || tdensity == 0 || sdensity == tdensity) { 143 return size; 144 } 145 146 // Scale by tdensity / sdensity, rounding up. 147 return ((size * tdensity) + (sdensity >> 1)) / sdensity; 148 } 149 150 /** 151 * Asserts that two images are similar within the given thresholds. 152 * 153 * @param message Error message 154 * @param expected Expected bitmap 155 * @param actual Actual bitmap 156 * @param pixelThreshold The total difference threshold for a single pixel 157 * @param pixelCountThreshold The total different pixel count threshold 158 * @param pixelDiffTolerance The pixel value difference tolerance 159 * 160 */ compareImages(String message, Bitmap expected, Bitmap actual, float pixelThreshold, float pixelCountThreshold, int pixelDiffTolerance)161 public static void compareImages(String message, Bitmap expected, Bitmap actual, 162 float pixelThreshold, float pixelCountThreshold, int pixelDiffTolerance) { 163 int idealWidth = expected.getWidth(); 164 int idealHeight = expected.getHeight(); 165 166 Assert.assertTrue(idealWidth == actual.getWidth()); 167 Assert.assertTrue(idealHeight == actual.getHeight()); 168 169 int totalDiffPixelCount = 0; 170 float totalPixelCount = idealWidth * idealHeight; 171 for (int x = 0; x < idealWidth; x++) { 172 for (int y = 0; y < idealHeight; y++) { 173 int idealColor = expected.getPixel(x, y); 174 int givenColor = actual.getPixel(x, y); 175 if (idealColor == givenColor) 176 continue; 177 if (Color.alpha(idealColor) + Color.alpha(givenColor) == 0) { 178 continue; 179 } 180 181 float idealAlpha = Color.alpha(idealColor) / 255.0f; 182 float givenAlpha = Color.alpha(givenColor) / 255.0f; 183 184 // compare premultiplied color values 185 float totalError = 0; 186 totalError += Math.abs((idealAlpha * Color.red(idealColor)) 187 - (givenAlpha * Color.red(givenColor))); 188 totalError += Math.abs((idealAlpha * Color.green(idealColor)) 189 - (givenAlpha * Color.green(givenColor))); 190 totalError += Math.abs((idealAlpha * Color.blue(idealColor)) 191 - (givenAlpha * Color.blue(givenColor))); 192 totalError += Math.abs(Color.alpha(idealColor) - Color.alpha(givenColor)); 193 194 if ((totalError / 1024.0f) >= pixelThreshold) { 195 Assert.fail((message + ": totalError is " + totalError)); 196 } 197 198 if (totalError > pixelDiffTolerance) { 199 totalDiffPixelCount++; 200 } 201 } 202 } 203 if ((totalDiffPixelCount / totalPixelCount) >= pixelCountThreshold) { 204 Assert.fail((message +": totalDiffPixelCount is " + totalDiffPixelCount)); 205 } 206 } 207 208 /** 209 * Returns the {@link Color} at the specified location in the {@link Drawable}. 210 */ getPixel(Drawable d, int x, int y)211 public static int getPixel(Drawable d, int x, int y) { 212 final int w = Math.max(d.getIntrinsicWidth(), x + 1); 213 final int h = Math.max(d.getIntrinsicHeight(), y + 1); 214 final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 215 final Canvas c = new Canvas(b); 216 d.setBounds(0, 0, w, h); 217 d.draw(c); 218 219 final int pixel = b.getPixel(x, y); 220 b.recycle(); 221 return pixel; 222 } 223 224 /** 225 * Save a bitmap for debugging or golden image (re)generation purpose. 226 * The file name will be referred from the resource id, plus optionally {@code extras}, and 227 * "_golden" 228 */ saveAutoNamedVectorDrawableIntoPNG(@onNull Context context, @NonNull Bitmap bitmap, @IntegerRes int resId, @Nullable String extras)229 static void saveAutoNamedVectorDrawableIntoPNG(@NonNull Context context, @NonNull Bitmap bitmap, 230 @IntegerRes int resId, @Nullable String extras) 231 throws IOException { 232 String originalFilePath = context.getResources().getString(resId); 233 File originalFile = new File(originalFilePath); 234 String fileFullName = originalFile.getName(); 235 String fileTitle = fileFullName.substring(0, fileFullName.lastIndexOf(".")); 236 String outputFolder = context.getExternalFilesDir(null).getAbsolutePath(); 237 if (extras != null) { 238 fileTitle += "_" + extras; 239 } 240 saveVectorDrawableIntoPNG(bitmap, outputFolder, fileTitle); 241 } 242 243 /** 244 * Save a {@code bitmap} to the {@code fileFullName} plus "_golden". 245 */ saveVectorDrawableIntoPNG(@onNull Bitmap bitmap, @NonNull String outputFolder, @NonNull String fileFullName)246 static void saveVectorDrawableIntoPNG(@NonNull Bitmap bitmap, @NonNull String outputFolder, 247 @NonNull String fileFullName) 248 throws IOException { 249 // Save the image to the disk. 250 FileOutputStream out = null; 251 try { 252 File folder = new File(outputFolder); 253 if (!folder.exists()) { 254 folder.mkdir(); 255 } 256 String outputFilename = outputFolder + "/" + fileFullName + "_golden"; 257 outputFilename +=".png"; 258 File outputFile = new File(outputFilename); 259 if (!outputFile.exists()) { 260 outputFile.createNewFile(); 261 } 262 263 out = new FileOutputStream(outputFile, false); 264 bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); 265 Log.v(LOGTAG, "Write test No." + outputFilename + " to file successfully."); 266 } catch (Exception e) { 267 e.printStackTrace(); 268 } finally { 269 if (out != null) { 270 out.close(); 271 } 272 } 273 } 274 } 275