DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to take screenshots and generate PDFs in Kotlin

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)")
}
Enter fullscreen mode Exit fullscreen mode

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() }
}
Enter fullscreen mode Exit fullscreen mode

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() }
}
Enter fullscreen mode Exit fullscreen mode

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() }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)