blob: ef0fb76730b4aefb999429a2369b35cf737f0043 [file] [log] [blame]
Igor Deminf04f0d92021-07-13 16:13:27 +03001/*
2 * Copyright 2021 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
17package androidx.compose.ui.graphics
18
19import androidx.compose.ui.geometry.Size
20import androidx.compose.ui.geometry.isSpecified
21import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
22import androidx.compose.ui.graphics.drawscope.DrawScope
Igor Deminfbe070b2021-07-28 22:10:11 +030023import androidx.compose.ui.graphics.painter.BitmapPainter
Igor Deminf04f0d92021-07-13 16:13:27 +030024import androidx.compose.ui.graphics.painter.Painter
25import androidx.compose.ui.unit.Density
26import androidx.compose.ui.unit.IntSize
27import androidx.compose.ui.unit.LayoutDirection
Roman Sedaikin627d32c2021-09-03 16:18:11 +030028import org.jetbrains.skia.Bitmap
29import org.jetbrains.skia.ColorAlphaType
30import org.jetbrains.skia.ImageInfo
Igor Demin46505a92022-01-21 19:08:28 +030031import java.awt.Graphics
Igor Deminf04f0d92021-07-13 16:13:27 +030032import java.awt.Image
33import java.awt.Point
34import java.awt.image.AbstractMultiResolutionImage
35import java.awt.image.BufferedImage
36import java.awt.image.ColorModel
37import java.awt.image.DataBuffer
38import java.awt.image.DataBufferInt
39import java.awt.image.ImageObserver
40import java.awt.image.ImageProducer
41import java.awt.image.MultiResolutionImage
42import java.awt.image.Raster
43import java.awt.image.SinglePixelPackedSampleModel
Igor Demin46505a92022-01-21 19:08:28 +030044import kotlin.math.roundToInt
Igor Deminf04f0d92021-07-13 16:13:27 +030045
46/**
47 * Convert AWT [BufferedImage] to Compose [Painter], so it would be possible to pass it to Compose
48 * functions. Don't mutate [BufferedImage] after converting it to [Painter], it will lead to
49 * undefined behavior.
50 */
Igor Deminc6eb5382021-09-23 13:00:33 +030051@Deprecated("Use toPainter", replaceWith = ReplaceWith("toPainter()"))
Igor Deminf04f0d92021-07-13 16:13:27 +030052fun BufferedImage.asPainter(): Painter = BufferedImagePainter(this)
53
Igor Deminc6eb5382021-09-23 13:00:33 +030054/**
55 * Convert AWT [BufferedImage] to Compose [Painter], so it would be possible to pass it to Compose
56 * functions. Don't mutate [BufferedImage] after converting it to [Painter], because
57 * [BufferedImage] can be reused to avoid unnecessary conversions.
58 */
59fun BufferedImage.toPainter(): Painter = BufferedImagePainter(this)
60
Igor Deminf04f0d92021-07-13 16:13:27 +030061private class BufferedImagePainter(val image: BufferedImage) : Painter() {
Igor Deminc6eb5382021-09-23 13:00:33 +030062 private val bitmap by lazy { image.toComposeImageBitmap() }
Igor Deminf04f0d92021-07-13 16:13:27 +030063
64 override val intrinsicSize = Size(image.width.toFloat(), image.height.toFloat())
65
66 override fun DrawScope.onDraw() {
67 val intSize = IntSize(size.width.toInt(), size.height.toInt())
68 drawImage(bitmap, dstSize = intSize)
69 }
70}
71
72/**
73 * Convert Compose [Painter] to AWT [Image]. The result will not be rasterized right now, it
74 * will be rasterized when AWT will request the image with needed width and height, by calling
Igor Deminfbe070b2021-07-28 22:10:11 +030075 * [AbstractMultiResolutionImage.getResolutionVariant] on Windows/Linux, or
76 * [AbstractMultiResolutionImage.getResolutionVariants] on macOs.
Igor Deminf04f0d92021-07-13 16:13:27 +030077 *
78 * At the rasterization moment, [density] and [layoutDirection] will be passed to the painter.
79 * Usually most painters don't use them. Like the painters for svg/xml/raster resources:
80 * they don't use absolute '.dp' values to draw, they use values which are relative
81 * to their viewport.
82 *
Igor Demin46505a92022-01-21 19:08:28 +030083 * [density] also will be used to rasterize the default image, which can be used by some implementations
84 * (Tray icon on macOs, disabled icon for menu items)
85 *
Igor Deminf04f0d92021-07-13 16:13:27 +030086 * @param size the size of the [Image]
87 */
Igor Deminc6eb5382021-09-23 13:00:33 +030088@Deprecated(
89 "Use toAwtImage",
90 replaceWith = ReplaceWith("toAwtImage(density, layoutDirection, size)")
91)
Igor Deminf04f0d92021-07-13 16:13:27 +030092fun Painter.asAwtImage(
93 density: Density,
94 layoutDirection: LayoutDirection,
95 size: Size = intrinsicSize
Igor Deminc6eb5382021-09-23 13:00:33 +030096): Image = toAwtImage(density, layoutDirection, size)
97
98/**
99 * Convert Compose [Painter] to AWT [Image]. The result will not be rasterized right now, it
100 * will be rasterized when AWT will request the image with needed width and height, by calling
101 * [AbstractMultiResolutionImage.getResolutionVariant] on Windows/Linux, or
102 * [AbstractMultiResolutionImage.getResolutionVariants] on macOs.
103 *
104 * At the rasterization moment, [density] and [layoutDirection] will be passed to the painter.
105 * Usually most painters don't use them. Like the painters for svg/xml/raster resources:
106 * they don't use absolute '.dp' values to draw, they use values which are relative
107 * to their viewport.
108 *
109 * @param size the size of the [Image]
110 */
111fun Painter.toAwtImage(
112 density: Density,
113 layoutDirection: LayoutDirection,
114 size: Size = intrinsicSize
Igor Deminf04f0d92021-07-13 16:13:27 +0300115): Image {
116 require(size.isSpecified) {
117 "Cannot convert Painter with unspecified size. Please set size explicitly."
118 }
119 return PainterImage(
120 painter = this,
121 density = density,
122 layoutDirection = layoutDirection,
123 size = size
124 )
125}
126
127private class PainterImage(
128 private val painter: Painter,
129 private val density: Density,
130 private val layoutDirection: LayoutDirection,
Igor Deminfbe070b2021-07-28 22:10:11 +0300131 size: Size,
Igor Deminf04f0d92021-07-13 16:13:27 +0300132) : Image(), MultiResolutionImage {
Igor Deminfbe070b2021-07-28 22:10:11 +0300133 private val width = size.width.toInt()
134 private val height = size.height.toInt()
135 override fun getWidth(observer: ImageObserver?) = width
136 override fun getHeight(observer: ImageObserver?) = height
Igor Deminf04f0d92021-07-13 16:13:27 +0300137
138 override fun getResolutionVariant(
139 destImageWidth: Double,
140 destImageHeight: Double
141 ): Image {
142 val width = destImageWidth.toInt()
143 val height = destImageHeight.toInt()
144 return if (
145 painter is BufferedImagePainter &&
146 painter.image.width == width &&
147 painter.image.height == height
148 ) {
149 painter.image
150 } else {
Igor Deminc6eb5382021-09-23 13:00:33 +0300151 asBitmap(width, height).toAwtImage()
Igor Deminf04f0d92021-07-13 16:13:27 +0300152 }
153 }
154
155 private fun asBitmap(width: Int, height: Int): ImageBitmap {
156 val bitmap = ImageBitmap(width, height)
157 val canvas = Canvas(bitmap)
158 val floatSize = Size(width.toFloat(), height.toFloat())
159
160 CanvasDrawScope().draw(
161 density, layoutDirection, canvas, floatSize
162 ) {
163 with(painter) {
164 draw(floatSize)
165 }
166 }
167
168 return bitmap
169 }
170
171 override fun getProperty(name: String, observer: ImageObserver?): Any = UndefinedProperty
Igor Demin46505a92022-01-21 19:08:28 +0300172 override fun getSource(): ImageProducer = defaultImage.source
173 override fun getGraphics(): Graphics = defaultImage.graphics
Igor Deminf04f0d92021-07-13 16:13:27 +0300174
Igor Demin46505a92022-01-21 19:08:28 +0300175 private val defaultImage by lazy {
Igor Deminfbe070b2021-07-28 22:10:11 +0300176 // optimizations to avoid unnecessary rasterizations
177 when (painter) {
Igor Demin46505a92022-01-21 19:08:28 +0300178 is BufferedImagePainter -> painter.image
179 is BitmapPainter -> asBitmap(width, height).toAwtImage()
180 else -> asBitmap(
181 (width * density.density).roundToInt(),
182 (height * density.density).roundToInt()
183 ).toAwtImage()
Igor Deminfbe070b2021-07-28 22:10:11 +0300184 }
185 }
186
Igor Demin46505a92022-01-21 19:08:28 +0300187 override fun getResolutionVariants() = listOf(defaultImage)
Igor Deminf04f0d92021-07-13 16:13:27 +0300188}
189
190// TODO(demin): should we optimize toAwtImage/toBitmap? Currently we convert colors according to the
191// current colorModel. But we can get raw BufferedImage.getRaster() and set a different colorModel.
192
193/**
194 * Convert Compose [ImageBitmap] to AWT [BufferedImage]
195 */
Igor Deminc6eb5382021-09-23 13:00:33 +0300196
197@Deprecated("use toAwtImage", replaceWith = ReplaceWith("toAwtImage"))
198fun ImageBitmap.asAwtImage(): BufferedImage = toAwtImage()
199
200/**
201 * Convert Compose [ImageBitmap] to AWT [BufferedImage]
202 */
203fun ImageBitmap.toAwtImage(): BufferedImage {
Igor Deminf04f0d92021-07-13 16:13:27 +0300204 // TODO(demin): use asDesktopBitmap().toBufferedImage() from skiko, when we fix it. Currently
205 // some images convert with graphical artifacts
206
207 val pixels = IntArray(width * height)
208 readPixels(pixels)
209
210 val a = 0xff shl 24
211 val r = 0xff shl 16
212 val g = 0xff shl 8
213 val b = 0xff shl 0
214 val bitMasks = intArrayOf(r, g, b, a)
215 val sm = SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, width, height, bitMasks)
216 val db = DataBufferInt(pixels, pixels.size)
217 val wr = Raster.createWritableRaster(sm, db, Point())
218 return BufferedImage(ColorModel.getRGBdefault(), wr, false, null)
219}
220
221/**
222 * Convert AWT [BufferedImage] to Compose [ImageBitmap]
223 */
Igor Deminc6eb5382021-09-23 13:00:33 +0300224@Deprecated("use toComposeImageBitmap()", replaceWith = ReplaceWith("toComposeImageBitmap()"))
225fun BufferedImage.toComposeBitmap(): ImageBitmap = toComposeImageBitmap()
226
227/**
228 * Convert AWT [BufferedImage] to Compose [ImageBitmap]
229 */
230fun BufferedImage.toComposeImageBitmap(): ImageBitmap {
Igor Deminf04f0d92021-07-13 16:13:27 +0300231 // TODO(demin): use toBitmap().asImageBitmap() from skiko, when we fix its performance
232 // (it is 40x slower)
233
234 val bytesPerPixel = 4
235 val pixels = ByteArray(width * height * bytesPerPixel)
236
237 var k = 0
238 for (y in 0 until height) {
239 for (x in 0 until width) {
240 val argb = getRGB(x, y)
241 val a = (argb shr 24) and 0xff
242 val r = (argb shr 16) and 0xff
243 val g = (argb shr 8) and 0xff
244 val b = (argb shr 0) and 0xff
245 pixels[k++] = b.toByte()
246 pixels[k++] = g.toByte()
247 pixels[k++] = r.toByte()
248 pixels[k++] = a.toByte()
249 }
250 }
251
252 val bitmap = Bitmap()
253 bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.UNPREMUL))
254 bitmap.installPixels(pixels)
Igor Deminc6eb5382021-09-23 13:00:33 +0300255 return bitmap.asComposeImageBitmap()
Igor Deminf04f0d92021-07-13 16:13:27 +0300256}