← Back to list

Jetpack Compose Canvas to Bitmap: A Complete Guide

Have you ever drawn something beautiful on a Jetpack Compose Canvas and thought, “I wish I could save this as an image”? Whether you’re…

Chirag T · 2026-02-27 11:20 · 3 claps · 5.2 min read
#jetpack-compose #claude #bitmap #canvas #kotlin
Open on Medium ↗
Wiki topics: LLM · Large Language Models TLS · Design Tools & Workflow 📱 · Mobile Development

Jetpack Compose Canvas to Bitmap: A Complete Guide

Have you ever drawn something beautiful on a Jetpack Compose Canvas and thought, “I wish I could save this as an image”? Whether you’re building a drawing app, generating charts, or creating custom graphics, converting your Canvas drawings into a Bitmap is a skill you’ll need.

In this article, I’ll walk you through every step — from understanding the core concepts to building a fully working Android app that draws on a Canvas and exports it as a PNG image.

What You’ll Learn

  • How CanvasDrawScope works under the hood
  • How to draw on an off-screen Canvas and capture it as ImageBitmap
  • How to convert ImageBitmap to android.graphics.Bitmap
  • How to save the Bitmap as a PNG file to the device gallery
  • A complete project you can clone and run

Why Canvas to Bitmap?

Jetpack Compose’s Canvas composable is fantastic for rendering custom graphics — circles, paths, text, gradients — you name it. But the Canvas composable renders directly to the screen. It doesn't give you a Bitmap object you can save or share.

That’s where CanvasDrawScope comes in. It lets you perform the exact same drawing operations, but target an off-screen area ImageBitmap instead of the screen.

Step 1: Understanding the Key Classes

Before we write code, let’s understand three essential classes.

ImageBitmap

ImageBitmap is Compose's multiplatform representation of a bitmap. Think of it as a blank sheet of pixels you can draw on.

val imageBitmap = ImageBitmap(width = 800, height = 600)

Canvas

Canvas in androidx.compose.ui.graphics wraps the ImageBitmap and provides a drawing surface.

val canvas = Canvas(imageBitmap)

CanvasDrawScope

CanvasDrawScope bridges the gap between the Compose drawing API (DrawScope) and the raw Canvas. It lets you use familiar functions like drawCircle(), drawRect(), and drawPath() to draw onto any Canvas — not just the one attached to a composable.

val drawScope = CanvasDrawScope()

Step 2: Drawing onto an Off-Screen Bitmap

Here’s the core pattern. We create an ImageBitmap, wrap it in a Canvas, then use CanvasDrawScope to draw on it.

import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection

fun createBitmapFromCanvas(): ImageBitmap {
    val width = 800
    val height = 600
    // 1. Create a blank ImageBitmap
    val imageBitmap = ImageBitmap(width, height)
    // 2. Create a Canvas targeting this bitmap
    val canvas = Canvas(imageBitmap)
    // 3. Use CanvasDrawScope to draw
    val drawScope = CanvasDrawScope()
    drawScope.draw(
        density = Density(1f),
        layoutDirection = LayoutDirection.Ltr,
        canvas = canvas,
        size = Size(width.toFloat(), height.toFloat())
    ) {
        // Draw a background
        drawRect(color = Color(0xFF1A1A2E))
        // Draw a circle in the center
        drawCircle(
            color = Color.Cyan,
            radius = 150f,
            center = center
        )
        // Draw a smaller circle
        drawCircle(
            color = Color(0xFF7C4DFF),
            radius = 80f,
            center = center
        )
    }
    return imageBitmap
}

That’s it! The imageBitmap now contains your drawn graphics as pixel data.

Step 3: Converting ImageBitmap to Android Bitmap

Compose’s ImageBitmap and Android's android.graphics.Bitmap are different types. To save or share the image, you need the Android Bitmap. The conversion is straightforward:

import androidx.compose.ui.graphics.asAndroidBitmap
val androidBitmap: android.graphics.Bitmap = imageBitmap.asAndroidBitmap()

That one-liner handles the conversion. The asAndroidBitmap() extension function is available in androidx.compose.ui.graphics.

Step 4: Saving the Bitmap to Gallery

Now let’s save that Bitmap as a PNG file. On Android 10+ (API 29+), we use MediaStore:

import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import java.io.IOException

fun saveBitmapToGallery(context: Context, bitmap: Bitmap, fileName: String) {
    val contentValues = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "$fileName.png")
        put(MediaStore.Images.Media.MIME_TYPE, "image/png")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(
                MediaStore.Images.Media.RELATIVE_PATH,
                Environment.DIRECTORY_PICTURES + "/CanvasToBitmap"
            )
        }
    }
    val resolver = context.contentResolver
    val uri = resolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        contentValues
    )
    uri?.let {
        try {
            resolver.openOutputStream(it)?.use { outputStream ->
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
            }
            Toast.makeText(context, "Saved to Gallery!", Toast.LENGTH_SHORT).show()
        } catch (e: IOException) {
            Toast.makeText(context, "Failed to save: ${e.message}", Toast.LENGTH_SHORT).show()
        }
    }
}

Step 5: Building the Complete UI

Let’s put everything together in a single composable screen. The user will see a preview of the Canvas drawing, and a button to save it.

@Composable
fun CanvasToBitmapScreen() {
    val context = LocalContext.current
// Hold the generated bitmap in state
    var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
    // Generate bitmap on first composition
    LaunchedEffect(Unit) {
        imageBitmap = createBitmapFromCanvas()
    }
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFF0F0C29))
            .padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Canvas to Bitmap",
            color = Color.White,
            fontSize = 28.sp,
            fontWeight = FontWeight.Bold
        )
        Spacer(modifier = Modifier.height(24.dp))
        // Show the live Canvas preview
        Canvas(
            modifier = Modifier
                .size(300.dp, 225.dp)
                .clip(RoundedCornerShape(16.dp))
        ) {
            drawRect(color = Color(0xFF1A1A2E))
            drawCircle(
                color = Color.Cyan,
                radius = 150f,
                center = center
            )
            drawCircle(
                color = Color(0xFF7C4DFF),
                radius = 80f,
                center = center
            )
        }
        Spacer(modifier = Modifier.height(16.dp))
        // Show the bitmap preview if available
        imageBitmap?.let { bitmap ->
            Text(
                text = "Bitmap Preview (${bitmap.width} x ${bitmap.height})",
                color = Color.Gray,
                fontSize = 14.sp
            )
            Spacer(modifier = Modifier.height(8.dp))
            Image(
                bitmap = bitmap,
                contentDescription = "Generated Bitmap",
                modifier = Modifier
                    .size(300.dp, 225.dp)
                    .clip(RoundedCornerShape(16.dp))
            )
        }
        Spacer(modifier = Modifier.height(24.dp))
        // Save button
        Button(
            onClick = {
                imageBitmap?.let { composeBitmap ->
                    val androidBitmap = composeBitmap.asAndroidBitmap()
                    saveBitmapToGallery(
                        context = context,
                        bitmap = androidBitmap,
                        fileName = "canvas_drawing_${System.currentTimeMillis()}"
                    )
                }
            },
            colors = ButtonDefaults.buttonColors(
                containerColor = Color.Cyan
            ),
            shape = RoundedCornerShape(12.dp),
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)
        ) {
            Text(
                text = "Save to Gallery",
                color = Color.Black,
                fontWeight = FontWeight.Bold,
                fontSize = 16.sp
            )
        }
    }
}

Step 6: Drawing More Complex Graphics

The real power shows when you draw complex, dynamic graphics. Here’s a function that creates a more interesting bitmap with gradients, paths, and text:

fun createAdvancedBitmap(): ImageBitmap {
    val width = 1080
    val height = 1080
val imageBitmap = ImageBitmap(width, height)
    val canvas = Canvas(imageBitmap)
    val drawScope = CanvasDrawScope()
    drawScope.draw(
        density = Density(1f),
        layoutDirection = LayoutDirection.Ltr,
        canvas = canvas,
        size = Size(width.toFloat(), height.toFloat())
    ) {
        // Gradient background
        drawRect(
            brush = Brush.linearGradient(
                colors = listOf(
                    Color(0xFF0F0C29),
                    Color(0xFF302B63),
                    Color(0xFF24243E)
                )
            )
        )
        // Draw concentric circles
        val circleColors = listOf(
            Color(0x334FC3F7),
            Color(0x5500E676),
            Color(0x777C4DFF),
            Color(0xFFE040FB)
        )
        circleColors.forEachIndexed { index, color ->
            drawCircle(
                color = color,
                radius = (size.minDimension / 2) - (index * 100f),
                center = center
            )
        }
        // Draw diagonal lines
        for (i in 0..10) {
            drawLine(
                color = Color.White.copy(alpha = 0.1f),
                start = Offset(i * 108f, 0f),
                end = Offset(0f, i * 108f),
                strokeWidth = 2f
            )
        }
    }
    return imageBitmap
}

Step 7: Adding Text to Your Canvas Bitmap

Drawing text on a CanvasDrawScope requires using the native canvas, since DrawScope doesn't have a drawText method for raw strings. Here's how:

drawScope.draw(
    density = Density(3f), // Use 3f for high DPI
    layoutDirection = LayoutDirection.Ltr,
    canvas = canvas,
    size = Size(width.toFloat(), height.toFloat())
) {
    // ... draw your shapes first ...
// Access native canvas for text
    drawIntoCanvas { nativeCanvas ->
        val paint = android.graphics.Paint().apply {
            color = android.graphics.Color.WHITE
            textSize = 48f
            isAntiAlias = true
            textAlign = android.graphics.Paint.Align.CENTER
            typeface = android.graphics.Typeface.create(
                android.graphics.Typeface.DEFAULT,
                android.graphics.Typeface.BOLD
            )
        }
        nativeCanvas.nativeCanvas.drawText(
            "Hello from Canvas!",
            width / 2f,
            height / 2f,
            paint
        )
    }
}

Common Pitfalls and Tips

1. Density matters. If your bitmap looks pixelated, increase the Density value in drawScope.draw(). A density of 3f mimics an xxhdpi screen.

2. Don’t confuse the two Canvas classes. androidx.compose.ui.graphics.Canvas (Compose) and android.graphics.Canvas (Android) are different. Use the Compose one with CanvasDrawScope.

3. Memory considerations. Large bitmaps eat memory. A 4000x4000 ARGB_8888 bitmap uses ~64MB. Size your bitmaps appropriately.

4. Thread safety. Create bitmaps off the main thread for large sizes. Use withContext(Dispatchers.IO) in a coroutine for the save operation.

5. ImageBitmap vs Bitmap. Use imageBitmap.asAndroidBitmap() to convert from Compose to Android, and bitmap.asImageBitmap() for the reverse.

🍴Check out the complete code on my **GitHub** Project. ✍️ Hope this project helps you. Hope you enjoy coding Jetpack Compose 😁. Don’t forget to share 📨 and clap 👏.

Any Suggestions are welcome. If you need any help or have questions for Code Contact Me. You can follow me on **LinkedIn**, ***StackOverflow and [Twitter](https://twitter.com/chiragthummar22)* For More Updates 🔔

Happy Compose !! 🚀

[embed]GitHub - chiragthummar/CanvasToBitmapCompose Contribute to chiragthummar/CanvasToBitmapCompose development by creating an account on GitHub.github.com

Happy Compose !! 🚀


메타데이터
post_id
32649d3acbb4
slug
jetpack-compose-canvas-to-bitmap-a-complete-guide-32649d3acbb4
url
https://medium.com/@chiragthummar16/jetpack-compose-canvas-to-bitmap-a-complete-guide-32649d3acbb4
canonical_url
https://medium.com/@chiragthummar16/jetpack-compose-canvas-to-bitmap-a-complete-guide-32649d3acbb4
author_url
https://medium.com/@chiragthummar16
status
ok
fetched_at
2026-07-13 06:23:13