
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:
- Visual regression testing.
- Generating website previews.
- Archiving landing pages.
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!
Comments
No comments yet. Be the first to comment!
Leave a comment
Comments are moderated and may take some time to appear.