1 /*
<lambda>null2  * Copyright 2022 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 platform.test.screenshot.matchers
18 
19 import android.annotation.SuppressLint
20 import android.graphics.Bitmap
21 import android.graphics.Color
22 import android.graphics.Rect
23 import kotlin.collections.List
24 import platform.test.screenshot.proto.ScreenshotResultProto
25 import platform.test.screenshot.proto.ScreenshotResultProto.DiffResult.ComparisonStatistics
26 
27 /** The abstract class to implement to provide custom bitmap matchers. */
28 abstract class BitmapMatcher {
29     /**
30      * Compares the given bitmaps and returns result of the operation.
31      *
32      * The images need to have same size.
33      *
34      * @param expected The reference / golden image.
35      * @param given The image taken during the test.
36      * @param width Width of both of the images.
37      * @param height Height of both of the images.
38      * @param regions An optional array of interesting regions for screenshot diff.
39      */
40     abstract fun compareBitmaps(
41         expected: IntArray,
42         given: IntArray,
43         width: Int,
44         height: Int,
45         regions: List<Rect> = emptyList()
46     ): MatchResult
47 
48     /**
49      * Compares the given bitmaps with different dimensions and returns result of the operation.
50      *
51      * The two different-sized images are aligned at (0, 0), and the underlying matcher is the same
52      * as [PixelPerfectMatcher].
53      *
54      * @param expected The reference / golden image.
55      * @param given The image taken during the test.
56      * @param expectedWidth Width of the expected image.
57      * @param expectedHeight Height of the expected image.
58      * @param actualWidth Width of the actual image.
59      * @param actualHeight Height of the actual image.
60      */
61     open fun compareBitmaps(
62         expected: IntArray,
63         given: IntArray,
64         expectedWidth: Int,
65         expectedHeight: Int,
66         actualWidth: Int,
67         actualHeight: Int
68     ): MatchResult {
69         val width = if (expectedWidth < actualWidth) expectedWidth else actualWidth
70         val height = if (expectedHeight < actualHeight) expectedHeight else actualHeight
71         var different = 0
72         var same = 0
73 
74         val diffArray = lazy { IntArray(width * height) { Color.TRANSPARENT } }
75 
76         for (i in 0..<height) {
77             for (j in 0..<width) {
78                 val actualIndex = i * actualWidth + j
79                 val expectedIndex = i * expectedWidth + j
80                 if (expected[expectedIndex] == given[actualIndex]) {
81                     same++
82                 } else {
83                     different++
84                     diffArray.value[i * width + j] = Color.MAGENTA
85                 }
86             }
87         }
88 
89         val stats =
90             ScreenshotResultProto.DiffResult.ComparisonStatistics.newBuilder()
91                 .setNumberPixelsCompared(width * height)
92                 .setNumberPixelsIdentical(same)
93                 .setNumberPixelsDifferent(different)
94                 .build()
95 
96         return if (different > 0) {
97             val diff = Bitmap.createBitmap(diffArray.value, width, height, Bitmap.Config.ARGB_8888)
98             MatchResult(matches = false, diff = diff, comparisonStatistics = stats)
99         } else {
100             MatchResult(matches = true, diff = null, comparisonStatistics = stats)
101         }
102     }
103 
104     @SuppressLint("CheckResult")
105     protected fun getFilter(width: Int, height: Int, regions: List<Rect>): BooleanArray {
106         return if (regions.isEmpty()) {
107             BooleanArray(width * height) { true }
108         } else {
109             val regionsSanitised = regions.map { Rect(it).apply { intersect(0, 0, width, height) } }
110             BooleanArray(width * height).also { filterArr ->
111                 regionsSanitised.forEach { region ->
112                     val startX = region.left.coerceIn(0, width - 1)
113                     val endX = region.right.coerceIn(0, width - 1)
114                     val startY = region.top.coerceIn(0, height - 1)
115                     val endY = region.bottom.coerceIn(0, height - 1)
116                     for (x in startX..endX) {
117                         for (y in startY..endY) {
118                             filterArr[y * width + x] = true
119                         }
120                     }
121                 }
122             }
123         }
124     }
125 }
126 
127 /**
128  * Result of the matching performed by [BitmapMatcher].
129  *
130  * @param matches True if bitmaps match.
131  * @param comparisonStatistics Matching statistics provided by this matcher that performed the
132  *   comparison.
133  * @param diff Diff bitmap that highlights the differences between the images. Can be null if match
134  *   was found.
135  */
136 class MatchResult(
137     val matches: Boolean,
138     val comparisonStatistics: ComparisonStatistics,
139     val diff: Bitmap?
140 )
141