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