kotlincanvascompose-desktopjetbrains-compose

Draw on Compose canvas by pixels


I have an image encoded as 2D array of colors like this:

val pixels = arrayOf(
  arrayOf(color1, color2,... colorN),
  ...
)

I would like to draw it pixel-by-pixel on Compose Canvas (desktop application, not Android):

    Canvas(
        modifier = Modifier
          .size(200.dp)
          .background(Color.White)
    ) {
        drawPixels(pixels) //need function like this
    }

The only way I found is to use drawPoints but it required list of points for every color. Not the best way if I have hundreds of different colors.

How can I draw an array of pixels?


Solution

  • Your array is effectively a bitmap, a list of colors for each pixel. Bitmaps can be drawn into a Canvas with drawImage.

    The only thing to do is to convert your array of arrays into the proper bitmap format:

    fun ImageBitmap(colors: Array<Array<Color>>): ImageBitmap {
        val bytesPerPixel = 4 // Alpha, Red, Green, Blue
    
        val width = colors.firstOrNull()?.size ?: 0
        val height = colors.size
    
        val bytes = ByteArray(width * height * bytesPerPixel)
        colors.forEachIndexed { x, row ->
            row.forEachIndexed { y, color ->
                with(color.convert(ColorSpaces.Srgb).value) {
                    repeat(bytesPerPixel) {
                        bytes[x * width * bytesPerPixel + y * bytesPerPixel + it] =
                            shr(32 + it * 8).toByte()
                    }
                }
            }
        }
    
        val image: Image = Image.makeRaster(
            imageInfo = ImageInfo.makeN32Premul(width, height),
            bytes = bytes,
            rowBytes = width * 4,
        )
    
        return image.toComposeImageBitmap()
    }
    

    This assumes your array contains colors of type androidx.compose.ui.graphics.Color.

    You can now simply draw this bitmap into the Canvas:

    val image = remember(pixels) { ImageBitmap(pixels) }
    
    Canvas(
        modifier = Modifier
            .size(200.dp)
            .background(Color.White)
    ) {
        drawImage(image)
    }
    

    On Android this would be a lot easier because we can create a android.graphics.Bitmap:

    import android.graphics.Bitmap
    
    fun <T> createBitmap(
        colors: Array<Array<T>>,
        transform: (T) -> Int,
        config: Bitmap.Config = Bitmap.Config.ARGB_8888,
    ): Bitmap {
        val intColors = colors
            .flatten()
            .map(transform)
            .toIntArray()
        val width = colors.firstOrNull()?.size ?: 0
        val height = colors.size
    
        return Bitmap.createBitmap(
            intColors,
            width,
            height,
            config,
        )
    }
    

    It can also use other color types as input. For an array of androidx.compose.ui.graphics.Colors the bitmap would be created like this:

    val bitmap = createBitmap(pixels, Color::toArgb)
    

    The config parameter is used to specify the type of the bitmap. The default is ARGB_8888 (simple 8 bit sRGB values with alpha) which should be a good fit for a Canvas.

    You can now simply draw this bitmap into the Canvas:

    Canvas(
        modifier = Modifier
            .size(200.dp)
            .background(Color.White)
    ) {
        drawImage(
            image = bitmap.asImageBitmap(),
        )
    }
    

    If you call createBitmap from a composable make sure you remember the result so it isn't recreated on every recomposition.