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)