Search code examples
android-jetpack-composecompose-desktopcompose-multiplatformscreenshot-testing

Using Jetpack Compose (Desktop), how can I render to an image without a window?


My use case that I'm attempting to create is a way to render a Compose application directly to a file, ideally without requiring an actual GUI window, e.g. for rendering the app on a server that does not have any UI.

Using Jetpack Compose (Desktop) I attempted different solutions to paint the window to a file, but it all ends up with a white or empty image:

val ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
val window = ComposeWindow(graphicsConfiguration = ge.defaultScreenDevice.defaultConfiguration)

window.setSize(200,200)
window.add(JLabel("Beispiel JLabel"))
window.setContent { App() }
window.repaint()
window.doLayout()
window.isVisible = true

val img = BufferedImage(window.width, window.height, BufferedImage.TYPE_INT_ARGB)

val g: Graphics2D = img.createGraphics()
window.printAll(g)
g.dispose()
val path: java.nio.file.Path = java.nio.file.Path.of("output.png")

ImageIO.write(img, "png", path.toFile())

Ideally, I would be able to skip creating a window altogether and let it render directly to e.g. a Skija surface. Could a unit test using Android as a target help here, as they provide a test rule for it?


Solution

  • I was able to create screenshot on Desktop (Windows) with Compose Multiplatform 1.5.0-beta01:

    import androidx.compose.material.Surface
    import androidx.compose.material.Text
    import androidx.compose.ui.graphics.asSkiaBitmap
    import androidx.compose.ui.test.ExperimentalTestApi
    import androidx.compose.ui.test.runDesktopComposeUiTest
    import org.jetbrains.skia.EncodedImageFormat
    import org.jetbrains.skia.Image
    import org.junit.Test
    import kotlin.io.path.Path
    import kotlin.io.path.readBytes
    import kotlin.io.path.writeBytes
    
    private val resourceAccessor = object {}
    
    fun getResourceAsPath(name: String): Path = resourceAccessor
        .javaClass
        .getResource("/$name")!!
        .toURI()
        .toPath()
    
    class ExampleUiTest {
        @Test
        // This is required
        @OptIn(ExperimentalTestApi::class)
        fun TakeExampleScreenshot() = runDesktopComposeUiTest(width = 200, height = 50) {
            setContent {
                // This Material surface provides a background color; can remove it
                Surface(color = Color.Red) {
                    // This is our composable under test
                    Text("Compose Multiplatform")
                }
            }
    
            val screenshot = Image.makeFromBitmap(captureToImage().asSkiaBitmap())
            val actualPath = Path("screenshot.png")
            val actualData = screenshot.encodeToData(EncodedImageFormat.PNG) ?: error("Could not encode image as png")
            actualPath.writeBytes(actualData.bytes)
    
            // Use this if reference is in the working directory (usually project root directory)
            val reference = Path("screenshot.png")
            // Use this if reference is in classpath (like src/main/resources/ or src/test/resources/)
            // val reference = getResourceAsPath("reference.png")
            // Another way to access classpath resource
            // val reference = ClassLoader.getSystemResource("reference.png")
    
            assert(actualPath.readBytes().contentEquals(reference.readBytes())) {
                "The screenshot '$actualPath' does not match the reference '$reference'"
            }
        }
    }
    

    Result screenshot:

    Result screenshot

    Thanks to this issue for its help.

    Also, see this example screenshot test and this issue.