1 /*
2  * Copyright 2023 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 package platform.test.screenshot
17 
18 import android.graphics.Bitmap
19 import java.util.stream.IntStream
20 
21 private val LIGHT_COLOR_MAPPING =
22     arrayOf(
23         -11640468 to -15111342,
24         -986893 to -1641480,
25     )
26 
27 private val DARK_COLOR_MAPPING =
28     arrayOf(
29         -16750965 to -12075112,
30         -16749487 to -10036302,
31         -15590111 to -1,
32         -15556945 to -11612698,
33         -15131618 to -15590111,
34         -14079187 to -1,
35         -11640468 to -8128307,
36         -4994349 to -13350318,
37         -4069121 to -8128307,
38         -3089183 to -16116455,
39         -1808030 to -16749487,
40         -986893 to -15590111,
41         -852993 to -13616321,
42         -1 to -1,
43     )
44 
45 private const val FILTER_SIZE = 2
46 
pixelWithinFilterRangenull47 private fun pixelWithinFilterRange(row: Int, col: Int, width: Int, height: Int): Boolean {
48     return (row >= FILTER_SIZE &&
49         row < height - FILTER_SIZE &&
50         col >= FILTER_SIZE &&
51         col < width - FILTER_SIZE)
52 }
53 
fillAverageColorForUnmappedPixelnull54 private fun fillAverageColorForUnmappedPixel(
55     bitmapArray: IntArray,
56     colorValidArray: IntArray,
57     row: Int,
58     col: Int,
59     bitmapWidth: Int
60 ) {
61     var validColorCount = 0
62     var validRedSum = 0
63     var validGreenSum = 0
64     var validBlueSum = 0
65     for (i in (row - FILTER_SIZE)..(row + FILTER_SIZE)) {
66         for (j in (col - FILTER_SIZE)..(col + FILTER_SIZE)) {
67             val pixelValue = colorValidArray[j + i * bitmapWidth]
68             if (pixelValue != 0) {
69                 validColorCount = validColorCount + 1
70                 validRedSum = validRedSum + ((pixelValue shr 16) and 0xFF)
71                 validGreenSum = validGreenSum + ((pixelValue shr 8) and 0xFF)
72                 validBlueSum = validBlueSum + (pixelValue and 0xFF)
73             }
74         }
75     }
76 
77     // If the valid color count of surrounding pixels is at least half of the total number,
78     // get their averages.
79     if (validColorCount > (FILTER_SIZE * 2 + 1) * (FILTER_SIZE * 2 + 1) / 2) {
80         val red = (validRedSum / validColorCount + 0.5).toInt()
81         val green = (validGreenSum / validColorCount + 0.5).toInt()
82         val blue = (validBlueSum / validColorCount + 0.5).toInt()
83         bitmapArray[col + row * bitmapWidth] =
84             ((0xFF shl 24) or // alpha
85             (red shl 16) or // red
86                 (green shl 8) or // green
87                 blue // blue
88             )
89     }
90 }
91 
92 /**
93  * Perform a Material You Color simulation for [originalBitmap] and return a bitmap after Material
94  * You simulation.
95  */
bitmapWithMaterialYouColorsSimulationnull96 fun bitmapWithMaterialYouColorsSimulation(
97     originalBitmap: Bitmap,
98     isDarkTheme: Boolean,
99     doPixelAveraging: Boolean = false,
100 ): Bitmap {
101     val bitmapArray = originalBitmap.toIntArray()
102     val mappingToUse = if (isDarkTheme) DARK_COLOR_MAPPING else LIGHT_COLOR_MAPPING
103 
104     // An array to indicate whether a pixel has valid mapping. If yes, its actual color appears in
105     // the array, otherwise 0 is stored.
106     val colorValidArray = IntArray(originalBitmap.width * originalBitmap.height, { 0 })
107     val stream = IntStream.range(0, originalBitmap.width * originalBitmap.height)
108     stream
109         .parallel()
110         .forEach({
111             val pixelValue = bitmapArray[it]
112             for (k in mappingToUse) {
113                 if (pixelValue == k.first) {
114                     bitmapArray[it] = k.second
115                     colorValidArray[it] = k.second
116                     break
117                 }
118             }
119         })
120 
121     if (doPixelAveraging) {
122         val newStream = IntStream.range(0, originalBitmap.width * originalBitmap.height)
123         newStream
124             .parallel()
125             .forEach({
126                 if (colorValidArray[it] == 0) {
127                     val col = it % originalBitmap.width
128                     val row = (it - col) / originalBitmap.width
129                     if (
130                         pixelWithinFilterRange(
131                             row,
132                             col,
133                             originalBitmap.width,
134                             originalBitmap.height
135                         )
136                     ) {
137                         fillAverageColorForUnmappedPixel(
138                             bitmapArray,
139                             colorValidArray,
140                             row,
141                             col,
142                             originalBitmap.width
143                         )
144                     }
145                 }
146             })
147     }
148 
149     return Bitmap.createBitmap(
150         bitmapArray,
151         originalBitmap.width,
152         originalBitmap.height,
153         originalBitmap.config
154     )
155 }
156