How to Take Screenshots and Generate PDFs in Scala
Scala inherits the JVM's headless browser problem. Options typically include Selenium WebDriver (requires ChromeDriver), scala-scraper (HTML parsing, not rendering), or bridging to a Node.js Puppeteer process. None of these are easy to manage in an Akka or ZIO application.
Here's the cleaner path: one HTTP call, binary response. Works in any Scala project — Play, Akka HTTP, http4s, or plain sbt run.
Using sttp (recommended)
sttp is the standard choice for synchronous and async HTTP in Scala:
// build.sbt
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % "4.0.0-RC1",
"com.softwaremill.sttp.client4" %% "circe" % "4.0.0-RC1",
"io.circe" %% "circe-generic" % "0.14.7"
)
import sttp.client4._
import sttp.client4.circe._
import io.circe.generic.auto._
object PageBolt {
private val apiKey = sys.env("PAGEBOLT_API_KEY")
private val baseUrl = "https://pagebolt.dev/api/v1"
private val backend = DefaultSyncBackend()
case class ScreenshotRequest(url: String, fullPage: Boolean = true, blockBanners: Boolean = true)
case class PdfRequest(url: Option[String] = None, html: Option[String] = None, blockBanners: Boolean = true)
def screenshot(url: String): Array[Byte] =
basicRequest
.post(uri"$baseUrl/screenshot")
.header("x-api-key", apiKey)
.body(ScreenshotRequest(url))
.response(asByteArrayAlways)
.send(backend)
.body
def pdfFromUrl(url: String): Array[Byte] =
basicRequest
.post(uri"$baseUrl/pdf")
.header("x-api-key", apiKey)
.body(PdfRequest(url = Some(url)))
.response(asByteArrayAlways)
.send(backend)
.body
def pdfFromHtml(html: String): Array[Byte] =
basicRequest
.post(uri"$baseUrl/pdf")
.header("x-api-key", apiKey)
.body(PdfRequest(html = Some(html)))
.response(asByteArrayAlways)
.send(backend)
.body
}
Play Framework controller
import play.api.mvc._
import play.api.libs.ws._
import javax.inject._
import scala.concurrent.{ExecutionContext, Future}
@Singleton
class InvoiceController @Inject() (
ws: WSClient,
cc: ControllerComponents
)(implicit ec: ExecutionContext) extends AbstractController(cc) {
private val apiKey = sys.env("PAGEBOLT_API_KEY")
private val baseUrl = "https://pagebolt.dev/api/v1"
def downloadPdf(id: Long) = Action.async {
val html = renderInvoiceTemplate(id)
ws.url(s"$baseUrl/pdf")
.withHttpHeaders("x-api-key" -> apiKey, "Content-Type" -> "application/json")
.post(s"""{"html":${ujson.Str(html)}}""")
.map { response =>
Ok(response.bodyAsBytes)
.as("application/pdf")
.withHeaders("Content-Disposition" -> s"""attachment; filename="invoice-$id.pdf"""")
}
}
}
Akka HTTP route
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model._
import akka.http.scaladsl.Http
import akka.stream.Materializer
import scala.concurrent.Future
class InvoiceRoutes(pagebolt: PageBoltAsync)(implicit mat: Materializer) {
val routes =
path("invoices" / LongNumber / "pdf") { id =>
get {
val html = renderInvoiceTemplate(id)
onSuccess(pagebolt.pdfFromHtml(html)) { pdfBytes =>
complete(
HttpResponse(
entity = HttpEntity(ContentType(MediaTypes.`application/pdf`), pdfBytes),
headers = List(
headers.`Content-Disposition`(
ContentDispositionTypes.attachment,
Map("filename" -> s"invoice-$id.pdf")
)
)
)
)
}
}
}
}
ZIO-based async client
import zio._
import zio.http._
object PageBoltZIO {
val screenshot: ZIO[Client, Throwable, Chunk[Byte]] =
for {
apiKey <- System.env("PAGEBOLT_API_KEY").someOrFail(new Exception("PAGEBOLT_API_KEY not set"))
response <- Client.batched(
Request.post(
url = URL.decode("https://pagebolt.dev/api/v1/screenshot").toOption.get,
body = Body.fromString("""{"url":"https://example.com","fullPage":true}""")
).addHeader("x-api-key", apiKey)
.addHeader(Header.ContentType(MediaType.application.json))
)
bytes <- response.body.asChunk
} yield bytes
}
No ChromeDriver, no Selenium, no browser process to supervise. sttp's synchronous backend works with any ExecutionContext and is straightforward to test — swap DefaultSyncBackend() for RecordingSttpBackend in tests.
Try it free — 100 requests/month, no credit card. → Get started in 2 minutes
Top comments (0)