How to Take Screenshots and Generate PDFs in Kotlin
Kotlin inherits the JVM's headless browser problem: Selenium WebDriver requires ChromeDriver, and the Kotlin Playwright bindings still download a full Chromium. Neither works easily in serverless or containerized environments with no display.
Here's the simpler path: one HTTP call, binary response. Works on any JVM and in Android apps.
Screenshot from a URL (OkHttp)
OkHttp is the dominant HTTP client in the Kotlin ecosystem (used by default in Android and Retrofit):
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
val client = OkHttpClient()
val apiKey = System.getenv("PAGEBOLT_API_KEY")
val baseUrl = "https://pagebolt.dev/api/v1"
fun screenshot(url: String): ByteArray {
val body = """{"url":"$url","fullPage":true,"blockBanners":true}"""
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("$baseUrl/screenshot")
.addHeader("x-api-key", apiKey)
.post(body)
.build()
return client.newCall(request).execute().use { it.body!!.bytes() }
}
fun main() {
val image = screenshot("https://example.com")
File("screenshot.png").writeBytes(image)
println("Screenshot saved (${image.size} bytes)")
}
PDF from a URL
fun pdfFromUrl(url: String): ByteArray {
val body = """{"url":"$url","blockBanners":true}"""
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("$baseUrl/pdf")
.addHeader("x-api-key", apiKey)
.post(body)
.build()
return client.newCall(request).execute().use { it.body!!.bytes() }
}
PDF from HTML
import org.json.JSONObject
fun pdfFromHtml(html: String): ByteArray {
val json = JSONObject().put("html", html).toString()
val body = json.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("$baseUrl/pdf")
.addHeader("x-api-key", apiKey)
.post(body)
.build()
return client.newCall(request).execute().use { it.body!!.bytes() }
}
JSONObject handles escaping — no manual string interpolation needed for HTML content.
Coroutine-friendly wrapper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class PageBoltClient {
private val http = OkHttpClient()
private val apiKey = System.getenv("PAGEBOLT_API_KEY")
private val baseUrl = "https://pagebolt.dev/api/v1"
private val json = "application/json".toMediaType()
suspend fun screenshot(url: String): ByteArray = withContext(Dispatchers.IO) {
val body = """{"url":"$url","fullPage":true,"blockBanners":true}"""
.toRequestBody(json)
http.newCall(
Request.Builder().url("$baseUrl/screenshot")
.addHeader("x-api-key", apiKey).post(body).build()
).execute().use { it.body!!.bytes() }
}
suspend fun pdfFromHtml(html: String): ByteArray = withContext(Dispatchers.IO) {
val bodyJson = JSONObject().put("html", html).toString()
.toRequestBody(json)
http.newCall(
Request.Builder().url("$baseUrl/pdf")
.addHeader("x-api-key", apiKey).post(bodyJson).build()
).execute().use { it.body!!.bytes() }
}
}
Ktor backend handler
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureRoutes(pagebolt: PageBoltClient) {
routing {
get("/invoices/{id}/pdf") {
val id = call.parameters["id"]!!
val html = renderInvoiceTemplate(id)
val pdf = pagebolt.pdfFromHtml(html)
call.response.header(
"Content-Disposition",
"attachment; filename=\"invoice-$id.pdf\""
)
call.respondBytes(pdf, contentType = io.ktor.http.ContentType.Application.Pdf)
}
}
}
Android — save PDF to Downloads
// In a ViewModel
viewModelScope.launch {
val html = renderInvoiceHtml(invoiceId)
val pdf = pagebolt.pdfFromHtml(html)
val filename = "invoice-$invoiceId.pdf"
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, filename)
put(MediaStore.Downloads.MIME_TYPE, "application/pdf")
}
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)!!
resolver.openOutputStream(uri)?.use { it.write(pdf) }
_uiState.value = UiState.PdfSaved(uri)
}
No WebView, no ChromeDriver, no Selenium. OkHttp ships with Android and is the standard choice for JVM HTTP — no additional runtime required.
Try it free — 100 requests/month, no credit card. → Get started in 2 minutes
Top comments (0)