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?
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:
Thanks to this issue for its help.
Also, see this example screenshot test and this issue.