How to Take Screenshots and Generate PDFs in Rust
Rust has no native headless browser. Teams that need screenshots or PDFs typically reach for headless_chrome (a Rust binding for Chrome DevTools Protocol), fantoccini (WebDriver), or shell out to a Puppeteer subprocess. All three require a running Chrome installation and add complexity to deployment.
Here's the simpler path: one HTTP call with reqwest, binary response, no browser.
Dependencies
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde_json = "1"
Screenshot from a URL
use reqwest::Client;
use std::env;
async fn screenshot(url: &str) -> Result<Vec<u8>, reqwest::Error> {
let api_key = env::var("PAGEBOLT_API_KEY").expect("PAGEBOLT_API_KEY not set");
let client = Client::new();
let body = serde_json::json!({
"url": url,
"fullPage": true,
"blockBanners": true
});
let bytes = client
.post("https://pagebolt.dev/api/v1/screenshot")
.header("x-api-key", api_key)
.json(&body)
.send()
.await?
.bytes()
.await?;
Ok(bytes.to_vec())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let image = screenshot("https://example.com").await?;
std::fs::write("screenshot.png", &image)?;
println!("Screenshot saved ({} bytes)", image.len());
Ok(())
}
PDF from a URL
async fn pdf_from_url(url: &str) -> Result<Vec<u8>, reqwest::Error> {
let api_key = env::var("PAGEBOLT_API_KEY").expect("PAGEBOLT_API_KEY not set");
let client = Client::new();
let bytes = client
.post("https://pagebolt.dev/api/v1/pdf")
.header("x-api-key", api_key)
.json(&serde_json::json!({ "url": url, "blockBanners": true }))
.send()
.await?
.bytes()
.await?;
Ok(bytes.to_vec())
}
PDF from HTML
async fn pdf_from_html(html: &str) -> Result<Vec<u8>, reqwest::Error> {
let api_key = env::var("PAGEBOLT_API_KEY").expect("PAGEBOLT_API_KEY not set");
let client = Client::new();
let bytes = client
.post("https://pagebolt.dev/api/v1/pdf")
.header("x-api-key", api_key)
.json(&serde_json::json!({ "html": html }))
.send()
.await?
.bytes()
.await?;
Ok(bytes.to_vec())
}
serde_json::json! handles escaping for you — no manual string building.
Reusable client struct
use reqwest::Client;
use std::env;
pub struct PageBolt {
client: Client,
api_key: String,
}
impl PageBolt {
pub fn new() -> Self {
Self {
client: Client::new(),
api_key: env::var("PAGEBOLT_API_KEY").expect("PAGEBOLT_API_KEY not set"),
}
}
pub async fn screenshot(&self, url: &str) -> Result<Vec<u8>, reqwest::Error> {
let bytes = self.client
.post("https://pagebolt.dev/api/v1/screenshot")
.header("x-api-key", &self.api_key)
.json(&serde_json::json!({ "url": url, "fullPage": true, "blockBanners": true }))
.send()
.await?
.bytes()
.await?;
Ok(bytes.to_vec())
}
pub async fn pdf_from_url(&self, url: &str) -> Result<Vec<u8>, reqwest::Error> {
let bytes = self.client
.post("https://pagebolt.dev/api/v1/pdf")
.header("x-api-key", &self.api_key)
.json(&serde_json::json!({ "url": url }))
.send()
.await?
.bytes()
.await?;
Ok(bytes.to_vec())
}
pub async fn pdf_from_html(&self, html: &str) -> Result<Vec<u8>, reqwest::Error> {
let bytes = self.client
.post("https://pagebolt.dev/api/v1/pdf")
.header("x-api-key", &self.api_key)
.json(&serde_json::json!({ "html": html }))
.send()
.await?
.bytes()
.await?;
Ok(bytes.to_vec())
}
}
Actix-web handler
use actix_web::{get, web, HttpResponse, Result};
#[get("/invoices/{id}/pdf")]
async fn invoice_pdf(
path: web::Path<u64>,
pagebolt: web::Data<PageBolt>,
invoice_service: web::Data<InvoiceService>,
) -> Result<HttpResponse> {
let id = path.into_inner();
let html = invoice_service.render_html(id).await?;
let pdf = pagebolt
.pdf_from_html(&html)
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
Ok(HttpResponse::Ok()
.content_type("application/pdf")
.append_header((
"Content-Disposition",
format!("attachment; filename=\"invoice-{}.pdf\"", id),
))
.body(pdf))
}
No browser, no ChromeDriver, no headless_chrome with its ChromeDriver version pinning. The reqwest client used here is the standard choice for async HTTP in Rust — it compiles to a single binary with no external runtime.
Try it free — 100 requests/month, no credit card. → Get started in 2 minutes
Top comments (0)