1 /*
2  * Copyright 2019 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 androidx.compose.ui.graphics
18 
19 import android.graphics.Bitmap
20 import android.graphics.BitmapFactory
21 import android.os.Build
22 import android.util.DisplayMetrics
23 import androidx.annotation.RequiresApi
24 import androidx.compose.ui.graphics.colorspace.ColorSpace
25 import androidx.compose.ui.graphics.colorspace.ColorSpaces
26 
27 /**
28  * Create an [ImageBitmap] from the given [Bitmap]. Note this does not create a copy of the original
29  * [Bitmap] and changes to it will modify the returned [ImageBitmap]
30  */
asImageBitmapnull31 fun Bitmap.asImageBitmap(): ImageBitmap = AndroidImageBitmap(this)
32 
33 internal actual fun createImageBitmap(bytes: ByteArray): ImageBitmap {
34     return BitmapFactory.decodeByteArray(bytes, 0, bytes.size).asImageBitmap()
35 }
36 
ActualImageBitmapnull37 internal actual fun ActualImageBitmap(
38     width: Int,
39     height: Int,
40     config: ImageBitmapConfig,
41     hasAlpha: Boolean,
42     colorSpace: ColorSpace
43 ): ImageBitmap {
44     val bitmapConfig = config.toBitmapConfig()
45     val bitmap: Bitmap
46     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
47         bitmap = Api26Bitmap.createBitmap(width, height, config, hasAlpha, colorSpace)
48     } else {
49         bitmap = Bitmap.createBitmap(null as DisplayMetrics?, width, height, bitmapConfig)
50         bitmap.setHasAlpha(hasAlpha)
51     }
52     return AndroidImageBitmap(bitmap)
53 }
54 
55 /**
56  * @Throws UnsupportedOperationException if this [ImageBitmap] is not backed by an
57  *   android.graphics.Bitmap
58  */
asAndroidBitmapnull59 fun ImageBitmap.asAndroidBitmap(): Bitmap =
60     when (this) {
61         is AndroidImageBitmap -> bitmap
62         else -> throw UnsupportedOperationException("Unable to obtain android.graphics.Bitmap")
63     }
64 
65 internal class AndroidImageBitmap(internal val bitmap: Bitmap) : ImageBitmap {
66 
67     override val width: Int
68         get() = bitmap.width
69 
70     override val height: Int
71         get() = bitmap.height
72 
73     override val config: ImageBitmapConfig
74         get() = bitmap.config!!.toImageConfig()
75 
76     override val colorSpace: ColorSpace
77         get() =
78             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
<lambda>null79                 with(Api26Bitmap) { bitmap.composeColorSpace() }
80             } else {
81                 ColorSpaces.Srgb
82             }
83 
readPixelsnull84     override fun readPixels(
85         buffer: IntArray,
86         startX: Int,
87         startY: Int,
88         width: Int,
89         height: Int,
90         bufferOffset: Int,
91         stride: Int
92     ) {
93         // Internal Android implementation that copies the pixels from the underlying
94         // android.graphics.Bitmap if the configuration supports it
95         val androidBitmap = asAndroidBitmap()
96         var recycleTarget = false
97         val targetBitmap =
98             if (
99                 Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
100                     androidBitmap.config != Bitmap.Config.HARDWARE
101             ) {
102                 androidBitmap
103             } else {
104                 // Because we are creating a copy for the purposes of reading pixels out of it
105                 // be sure to recycle this temporary bitmap when we are finished with it.
106                 recycleTarget = true
107 
108                 // Pixels of a hardware bitmap cannot be queried directly so make a copy
109                 // of it into a configuration that can be queried
110                 // Passing in false for the isMutable parameter as we only intend to read pixel
111                 // information from the bitmap
112                 androidBitmap.copy(Bitmap.Config.ARGB_8888, false)
113             }
114 
115         targetBitmap.getPixels(buffer, bufferOffset, stride, startX, startY, width, height)
116         // Recycle the target if we are done with it
117         if (recycleTarget) {
118             targetBitmap.recycle()
119         }
120     }
121 
122     override val hasAlpha: Boolean
123         get() = bitmap.hasAlpha()
124 
prepareToDrawnull125     override fun prepareToDraw() {
126         bitmap.prepareToDraw()
127     }
128 }
129 
toBitmapConfignull130 internal fun ImageBitmapConfig.toBitmapConfig(): Bitmap.Config {
131     // Cannot utilize when statements with enums that may have different sets of supported
132     // values between the compiled SDK and the platform version of the device.
133     // As a workaround use if/else statements
134     // See https://youtrack.jetbrains.com/issue/KT-30473 for details
135     return if (this == ImageBitmapConfig.Argb8888) {
136         Bitmap.Config.ARGB_8888
137     } else if (this == ImageBitmapConfig.Alpha8) {
138         Bitmap.Config.ALPHA_8
139     } else if (this == ImageBitmapConfig.Rgb565) {
140         Bitmap.Config.RGB_565
141     } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this == ImageBitmapConfig.F16) {
142         Bitmap.Config.RGBA_F16
143     } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this == ImageBitmapConfig.Gpu) {
144         Bitmap.Config.HARDWARE
145     } else {
146         Bitmap.Config.ARGB_8888
147     }
148 }
149 
toImageConfignull150 internal fun Bitmap.Config.toImageConfig(): ImageBitmapConfig {
151     // Cannot utilize when statements with enums that may have different sets of supported
152     // values between the compiled SDK and the platform version of the device.
153     // As a workaround use if/else statements
154     // See https://youtrack.jetbrains.com/issue/KT-30473 for details
155     @Suppress("DEPRECATION")
156     return if (this == Bitmap.Config.ALPHA_8) {
157         ImageBitmapConfig.Alpha8
158     } else if (this == Bitmap.Config.RGB_565) {
159         ImageBitmapConfig.Rgb565
160     } else if (this == Bitmap.Config.ARGB_4444) {
161         ImageBitmapConfig.Argb8888 // Always upgrade to Argb_8888
162     } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this == Bitmap.Config.RGBA_F16) {
163         ImageBitmapConfig.F16
164     } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this == Bitmap.Config.HARDWARE) {
165         ImageBitmapConfig.Gpu
166     } else {
167         ImageBitmapConfig.Argb8888
168     }
169 }
170 
171 /**
172  * Make Lint happy Separate class to contain all API calls that require API level 26 to assist in
173  * dead code elimination during compilation time
174  */
175 @RequiresApi(Build.VERSION_CODES.O)
176 internal object Api26Bitmap {
177     @JvmStatic
createBitmapnull178     internal fun createBitmap(
179         width: Int,
180         height: Int,
181         bitmapConfig: ImageBitmapConfig,
182         hasAlpha: Boolean,
183         colorSpace: ColorSpace
184     ): Bitmap {
185         // Note intentionally ignoring density in all cases
186         return Bitmap.createBitmap(
187             null,
188             width,
189             height,
190             bitmapConfig.toBitmapConfig(),
191             hasAlpha,
192             colorSpace.toAndroidColorSpace()
193         )
194     }
195 
196     @JvmStatic
composeColorSpacenull197     internal fun Bitmap.composeColorSpace() = colorSpace?.toComposeColorSpace() ?: ColorSpaces.Srgb
198 }
199