bandarra.me

Beyond the Viewport: Capturing Full-Size Screenshots with Rust and Chrome

If you’ve ever tried to automate website screenshots using Selenium or WebDriver, you’ve likely hit the "cutoff" wall. By default, most drivers only capture what’s currently visible in the browser window. If your page is 5,000 pixels long, but your window is only 1,080, you’re missing the best part of the story.

In this post, we’re going to look at how to use the Chrome DevTools Protocol (CDP) via the thirtyfour crate to capture every single pixel of a webpage, from header to footer.

Why standard screenshots fail

Standard WebDriver commands are designed for cross-browser compatibility. Because not every browser handles "full-page" rendering the same way, the lowest common denominator is the Viewport.

To get the full page in Chrome, we need to go "under the hood" and talk to Chrome directly using CDP.

The Secret Sauce: Page.captureScreenshot

Chrome provides a specific command called Page.captureScreenshot. The real hero here is a parameter called captureBeyondViewport. When set to true, Chrome ignores the window constraints and renders the full height of the document.

The Implementation

First, we define our data structures to match the Chrome DevTools schema. Note the #[serde(rename_all = "camelCase")] attribute. This is vital because Rust's snake_case will be rejected by Chrome's API.

use std::error::Error;
use base64::{Engine, prelude::BASE64_STANDARD};
use serde::{Deserialize, Serialize};
use thirtyfour::extensions::cdp::ChromeDevTools;

#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScreenshotParams {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub format: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub quality: Option<u8>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub clip: Option<Viewport>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from_surface: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub capture_beyond_viewport: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub optimize_for_speed: Option<bool>,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Viewport {
    pub x: u32,
    pub y: u32,
    pub width: u32,
    pub height: u32,
    pub scale: u32,
}

pub const FULL_SIZE_SCREENSHOT: ScreenshotParams = ScreenshotParams {
    capture_beyond_viewport: Some(true),
    from_surface: Some(true),
    clip: None,
    format: None,
    optimize_for_speed: None,
    quality: None,
};

Executing the Command

Once our structs are ready, we execute the command. Chrome returns the image as a Base64 encoded string, so we need to decode that into a raw byte vector (Vec<u8>) so we can save it to disk or process it.

pub async fn screenshot(devtools: &ChromeDevTools) -> Result<Vec<u8>, Box<dyn Error>> {
    // 1. Serialize our parameters to JSON
    let params = serde_json::to_value(&FULL_SIZE_SCREENSHOT).unwrap();

    // 2. Call the CDP method
    let response = devtools
        .execute_cdp_with_params("Page.captureScreenshot", params)
        .await?;

    // 3. Extract the Base64 data from the response
    let base_64_png = response.get("data")
        .and_then(|d| d.as_str())
        .ok_or("Failed to find image data in response")?;

    // 4. Decode it into raw PNG bytes
    let png = BASE64_STANDARD.decode(base_64_png)?;
    Ok(png)
}

How to use it in your project

Integrating this into your thirtyfour workflow is straightforward. Simply wrap your driver handle in a ChromeDevTools instance:

// ... setup your thirtyfour WebDriver ...
let devtools = ChromeDevTools::new(driver.handle());

// Navigate to a long page
driver.goto("[https://www.rust-lang.org](https://www.rust-lang.org)").await?;

// Capture everything!
let image_bytes = screenshot(&devtools).await?;
std::fs::write("rust_homepage.png", image_bytes)?;

Summary

By reaching past the standard WebDriver API and using CDP, we gain much finer control over how Chrome behaves. This approach is perfect for:

Just a heads-up: capturing extremely long pages (like a social media feed) can result in massive PNG files, so keep an eye on your memory usage!

Happy Hacking!

Note: This post was created with the assistance of AI. While a human carefully reviewed and edited the content, it's important to remember that AI tools may introduce errors or biases. If you have any concerns or questions, please feel free to reach out.

Comments

No comments yet. Be the first to comment!

Leave a comment

Comments are moderated and may take some time to appear.

You may also want to read