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