Selenium Screenshots in Python, Java and C# (Selenium 4)
Take a screenshot with Selenium 4 in Python, Java and C#: page, element, exact viewport and PDF. Same test in three languages, timed, with the fixes.
Selenium 4 takes a screenshot with one call in every language binding. All three write a PNG of the visible viewport:
driver.save_screenshot('page.png') # Python
Files.write(Path.of("page.png"), driver.getScreenshotAs(OutputType.BYTES)); // Java
driver.GetScreenshot().SaveAsFile("page.png"); // C#
That line is the easy part. The hard part is everything around it: the image is not the size you asked for, the format is always PNG, the page may not be ready, and full-page capture is a different problem in each browser. This guide runs one test program in Python, Java and C#, shows where the three bindings differ, and gives the numbers we measured. For other ways to capture a page (the Chrome command line, Puppeteer, Playwright, an API), start at the guide to taking website screenshots with code.
Tested on 2026-10-18 on an Apple M1 Pro, macOS 27.0.1, Google Chrome 155.0.8059.80 in headless mode. Python 3.15.0 with
selenium==4.51.0; Java 21.0.2 withorg.seleniumhq.selenium:selenium-java:4.51.0(Maven 3.9.16); .NET SDK 10.0.401 withSelenium.WebDriverandSelenium.Support4.51.0. Target:https://example.com. All three programs below produced the same 1280×800 PNG (81,121 bytes), the same 416×77 element PNG, and a one-page PDF.
The screenshot API in each binding
Selenium's bindings follow the same WebDriver commands, but the method names differ more than you might expect. This is the map we used while writing the three programs:
| Task | Python | Java | C# |
|---|---|---|---|
| Viewport to file | driver.save_screenshot(path) |
driver.getScreenshotAs(OutputType.FILE) then copy |
driver.GetScreenshot().SaveAsFile(path) |
| Viewport to bytes | driver.get_screenshot_as_png() |
driver.getScreenshotAs(OutputType.BYTES) |
driver.GetScreenshot().AsByteArray |
| Viewport to base64 | driver.get_screenshot_as_base64() |
driver.getScreenshotAs(OutputType.BASE64) |
driver.GetScreenshot().AsBase64EncodedString |
| One element | element.screenshot(path) / element.screenshot_as_png |
element.getScreenshotAs(OutputType.BYTES) |
((ITakesScreenshot)element).GetScreenshot() |
| Whole page as PDF | driver.print_page(PrintOptions()) (base64) |
driver.print(new PrintOptions()).getContent() (base64) |
driver.Print(new PrintOptions()).SaveAsFile(path) |
| Full page as image | Firefox only: driver.get_full_page_screenshot_as_file(path) |
Firefox only: ((FirefoxDriver) driver).getFullPageScreenshotAs(...) |
Firefox only: ((FirefoxDriver)driver).GetFullPageScreenshot() |
| Raw DevTools call | driver.execute_cdp_cmd(name, params) |
driver.executeCdpCommand(name, params) |
driver.ExecuteCdpCommand(name, params) |
The last two rows matter most. Chrome has no WebDriver command for a full-page image, so full-page capture in Chrome goes through the DevTools protocol. We tested the Firefox command and the Chrome clip method in depth in Selenium full page screenshots, so this guide does not repeat it. We did not run the Firefox rows for Java and C# on this date; the names come from the Selenium API reference.
Setup
Selenium 4.6 and later ship Selenium Manager, which finds or downloads a matching chromedriver for the Chrome you have. You do not install a driver by hand and you do not need webdriver-manager packages. Pin the Selenium version, because screenshot and print APIs have changed across 4.x releases.
# Python python3 -m venv .venv .venv/bin/pip install selenium==4.51.0
<!-- Java (Maven) --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.51.0</version> </dependency>
# C# (.NET) dotnet new console -n Shots cd Shots dotnet add package Selenium.WebDriver --version 4.51.0 dotnet add package Selenium.Support --version 4.51.0
One thing to know about Selenium Manager: during our runs the network could not reach the Chrome for Testing download index, and Selenium Manager printed There was an error managing chromedriver ... using driver found in the cache, then carried on. On a fresh CI runner with no cache and no network, that same situation is a hard failure. If your build machines are offline, install chromedriver in the image and point the service at it. Update it with Chrome, or the session fails with a ChromeDriver version mismatch.
One program, three languages
Each program does the same five things: start headless Chrome, set an exact 1280×800 viewport, load the page with a page-load timeout, wait for an element, then write a page PNG, an element PNG and a PDF. Each one closes the browser in a finally block, so a failed wait does not leave Chrome running.
Python
import base64 import sys from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.print_page_options import PrintOptions from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com' options = webdriver.ChromeOptions() options.add_argument('--headless=new') options.add_argument('--hide-scrollbars') driver = webdriver.Chrome(options=options) # Selenium Manager finds the driver try: # Exact 1280x800 viewport, whatever the window decorations are driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', { 'width': 1280, 'height': 800, 'deviceScaleFactor': 1, 'mobile': False, }) driver.set_page_load_timeout(30) driver.get(url) paragraph = WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, 'p')) ) driver.save_screenshot('page.png') paragraph.screenshot('paragraph.png') pdf = driver.print_page(PrintOptions()) # base64 with open('page.pdf', 'wb') as f: f.write(base64.b64decode(pdf)) print('wrote page.png, paragraph.png, page.pdf') finally: driver.quit()
Python has the most pitfalls of the three (a headless attribute that no longer does anything, .jpg names that still get PNG bytes), so it has its own guide: Selenium screenshots in Python.
Java
package demo; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.Base64; import java.util.Map; import org.openqa.selenium.By; import org.openqa.selenium.OutputType; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.print.PrintOptions; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; public class Screenshot { public static void main(String[] args) throws Exception { String url = args.length > 0 ? args[0] : "https://example.com"; ChromeOptions options = new ChromeOptions(); options.addArguments("--headless=new", "--hide-scrollbars"); ChromeDriver driver = new ChromeDriver(options); // Selenium Manager finds the driver try { // Exact 1280x800 viewport, whatever the window decorations are driver.executeCdpCommand("Emulation.setDeviceMetricsOverride", Map.of( "width", 1280, "height", 800, "deviceScaleFactor", 1, "mobile", false)); driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30)); driver.get(url); WebElement paragraph = new WebDriverWait(driver, Duration.ofSeconds(10)) .until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("p"))); Files.write(Path.of("page.png"), driver.getScreenshotAs(OutputType.BYTES)); Files.write(Path.of("paragraph.png"), paragraph.getScreenshotAs(OutputType.BYTES)); String pdf = driver.print(new PrintOptions()).getContent(); // base64 Files.write(Path.of("page.pdf"), Base64.getDecoder().decode(pdf)); System.out.println("wrote page.png, paragraph.png, page.pdf"); } finally { driver.quit(); } } }
Run it with mvn -q compile exec:java -Dexec.mainClass=demo.Screenshot (add the exec-maven-plugin to the POM). Two Java details: OutputType.FILE writes to a temporary file that you must copy or move yourself, and executeCdpCommand exists on ChromeDriver and EdgeDriver, not on the WebDriver interface. If your code holds a plain WebDriver, cast it or keep the concrete type, as above.
C
using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Support.UI; var url = args.Length > 0 ? args[0] : "https://example.com"; var options = new ChromeOptions(); options.AddArgument("--headless=new"); options.AddArgument("--hide-scrollbars"); using var driver = new ChromeDriver(options); // Selenium Manager finds the driver try { // Exact 1280x800 viewport, whatever the window decorations are driver.ExecuteCdpCommand("Emulation.setDeviceMetricsOverride", new Dictionary<string, object?> { ["width"] = 1280, ["height"] = 800, ["deviceScaleFactor"] = 1, ["mobile"] = false, }); driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30); driver.Navigate().GoToUrl(url); var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); var paragraph = wait.Until(d => d.FindElement(By.CssSelector("p"))); driver.GetScreenshot().SaveAsFile("page.png"); ((ITakesScreenshot)paragraph).GetScreenshot().SaveAsFile("paragraph.png"); driver.Print(new PrintOptions()).SaveAsFile("page.pdf"); Console.WriteLine("wrote page.png, paragraph.png, page.pdf"); } finally { driver.Quit(); }
Run it with dotnet run. Selenium 4.51 for .NET has nullable annotations, so the CDP parameter dictionary must be Dictionary<string, object?>. With object the build prints warning CS8620. In C#, IWebElement does not expose GetScreenshot() directly, so cast the element to ITakesScreenshot. Selenium.Support is a separate package and it holds WebDriverWait.
The viewport is not the window
This is the bug we see most in Selenium screenshot code. You set the window to 1280×800 and expect a 1280×800 image. We measured what headless Chrome 155 actually returns:
| How the size was set | Image size | innerWidth × innerHeight |
outerWidth × outerHeight |
|---|---|---|---|
| Nothing | 756×413 | 756×413 | 756×556 |
--window-size=1280,800 argument |
1280×657 | 1280×657 | 1280×800 |
set_window_size(1280, 800) |
1280×657 | 1280×657 | 1280×800 |
--window-size=1280,800 plus --force-device-scale-factor=2 |
2560×1314 | 1280×657 | 1280×800 |
Emulation.setDeviceMetricsOverride 1280×800 |
1280×800 | 1280×800 | n/a |
Headless Chrome reserves 143 px of height for browser UI that it does not draw. The same window in headed mode on a Retina Mac gave the same 657 px viewport, at device scale factor 2. Note also the default: with no size at all, the screenshot is a small 756×413 image, which is rarely what anyone wants.
You have two fixes:
- Override the device metrics with the DevTools command, as in all three programs above. It sets the exact viewport and the device scale factor in one call. It works in Chrome and Edge only.
- Measure and resize: read
outerHeight - innerHeightwithexecute_script, then callset_window_size(width + dx, height + dy). It works in any browser. The Python guide has a tested helper.
Do not use the plain Chrome command line as a reference here. chrome --headless --window-size=1280,800 --screenshot gives a 1280×800 image, because that path sizes the view, not a window. Selenium goes through a window, so the two disagree by 143 px.
Element screenshots
An element screenshot crops the page image to the element's bounding box. On example.com the first paragraph came out as 416×77 in all three languages. Things to know:
- Selenium scrolls the element into view first. An element taller than the viewport is still captured whole in current Chrome: a 2,000 px tall element in an 800 px viewport came back as a 300×2000 image.
- Fixed headers and cookie bars that overlap the element are in the crop. Hide them with
execute_scriptbefore the capture. - The element must have a size. Wait for visibility, not presence, as in the programs above. A zero-size element fails with
Cannot take screenshot with 0 width.
PDF with the print command
print_page, print and Print call the WebDriver print command, which uses Chrome's print-to-PDF. You get a vector PDF with selectable text, laid out with print CSS. In our runs the PDF of example.com was 191,859 bytes from Python and C# and 191,886 bytes from Java; the difference is metadata, not content.
PrintOptions controls the result: orientation, scale, background, shrink_to_fit, page width and height, margins and page ranges. The default page is US Letter (the PDF's media box was 612×792 points), set in centimetres by the WebDriver spec. Set the page width and height explicitly if your users print on A4.
The print command also worked in a headed Chrome 155 window in our test. Older guides say it needs headless mode; that is no longer true for Chrome.
Formats, sizes and scale
WebDriver screenshots are PNG only. There is no format or quality argument in any binding. We converted the 1280×800 example.com PNG with Pillow 13.0 to see what other formats would cost:
| Output | Bytes | Relative to PNG |
|---|---|---|
| PNG from Selenium | 81,121 | 100% |
| JPEG, quality 80 (Pillow) | 64,665 | 80% |
| WebP, quality 80 (Pillow) | 35,540 | 44% |
| PNG at device scale factor 2 (2560×1314) | 172,454 | 213% |
For retina images, set deviceScaleFactor: 2 in the device metrics override, or pass --force-device-scale-factor=2. The image doubles in each dimension, and the file roughly doubles in size for a simple page like this one. Photo-heavy pages grow more.
What a screenshot costs in time
We timed the Python program's steps in one process, with time.perf_counter(). Each row is the median of 5 runs, after one warm-up run that we did not count.
| Step | Median | Range |
|---|---|---|
| Start Chrome, load page, screenshot, quit | 1.018 s | 0.925 to 1.075 s |
| Load page and screenshot, browser already open | 0.067 s | 0.054 to 0.097 s |
get_screenshot_as_png() only |
0.048 s | 0.042 to 0.069 s |
| Element screenshot only | 0.066 s | 0.049 to 0.083 s |
print_page() only |
0.114 s | 0.113 to 0.169 s |
The capture itself is cheap. Starting and stopping Chrome is about 15 times the cost of the load and capture on a small page. If you take many screenshots, keep one driver open and visit pages in turn. Restart it every few hundred pages to drop leaked memory, and always call quit(); we measured what happens when you do not in headless Chrome production failures.
These numbers are from example.com, a page with almost nothing on it. They measure Selenium and Chrome overhead, not page rendering. A real page with fonts, images and scripts adds its own load time on top.
Waiting for the right moment
driver.get() returns when the page's load event fires (the normal page load strategy). That is too early for content that scripts add later, and too late for pages that hold one request open forever. Three settings decide what you capture:
- Page load timeout. The default is 300 seconds. Set it to something you would accept, such as 30 seconds, as all three programs do.
- Page load strategy.
eagerreturns atDOMContentLoaded. Use it when one slow third-party request blocksload, then wait for the element you need. - Explicit wait.
WebDriverWaitwith an expected condition, as above. Wait for the element that proves the page is ready, such as the chart or the product grid, not for a fixed sleep.
Fonts are a special case. Text can render in a fallback font if you capture before the web font arrives. Add driver.execute_async_script('document.fonts.ready.then(arguments[arguments.length - 1])') before the screenshot. Our font rendering study shows when even that is not enough.
Headless Chrome follows the OS theme
Our test Mac was set to Dark appearance. Headless Chrome reported prefers-color-scheme: dark to the page, and example.com, which supports both schemes, rendered dark grey. Your CI server, running Linux with no theme, renders light. That is a classic "works on my machine" screenshot diff.
Force the scheme you want. Both of these worked in our test:
# Option 1: a Chrome argument, before the driver starts options.add_argument('--blink-settings=preferredColorScheme=1') # 1 = light, 0 = dark # Option 2: a DevTools call, after the driver starts driver.execute_cdp_cmd('Emulation.setEmulatedMedia', { 'features': [{'name': 'prefers-color-scheme', 'value': 'light'}], })
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Image is 1280×657, not 1280×800 | Window size includes 143 px of hidden browser UI | Emulation.setDeviceMetricsOverride, or grow the window by outerHeight - innerHeight |
| Image is 756×413 | No window size set in headless mode | Set the viewport explicitly |
| A browser window opens although you asked for headless (Python) | options.headless = True no longer does anything in Selenium 4.10+ |
options.add_argument('--headless=new') |
File named .jpg is a PNG |
WebDriver returns PNG only | Convert with Pillow, ImageIO or ImageSharp |
| Screenshot is dark on a laptop, light in CI | Headless Chrome follows the OS theme | --blink-settings=preferredColorScheme=1 or Emulation.setEmulatedMedia |
TimeoutException: Timed out receiving message from renderer |
A request never finishes, so load never fires |
Lower page-load timeout, eager strategy, wait for an element; you can still take the screenshot after the exception |
NoSuchElementException before the screenshot |
The selector is wrong, or the element is added later | WebDriverWait for visibility; check the page source |
Cannot take screenshot with 0 width. |
The element exists but has no size yet | Wait for visibility, not presence |
executeCdpCommand does not compile (Java) |
Variable typed as WebDriver |
Keep it as ChromeDriver or cast it |
CS8620 warning on ExecuteCdpCommand (C#) |
Nullable annotations in Selenium 4.51 | Use Dictionary<string, object?> |
There was an error managing chromedriver |
Selenium Manager could not reach the download index | It falls back to its cache; on offline runners install chromedriver in the image |
| PDF is Letter size, you need A4 | WebDriver default page size | Set page width 21.0 and height 29.7 (cm) in PrintOptions |
The same capture with one API call
If Selenium is in your stack only to take screenshots, you maintain a browser, a driver and a viewport workaround for one image. With RenderScreenshot the viewport you send is the viewport you get, and the format is a parameter. In Python, with the Python SDK:
import os from renderscreenshot import Client, TakeOptions client = Client(os.environ['RS_API_KEY']) image = client.take( TakeOptions.url('https://example.com').width(1280).height(800).format('webp') ) with open('page.webp', 'wb') as f: f.write(image)
In Java, with the Java SDK, the same options are camel-case methods (more in the Java screenshot API guide):
Client client = new Client(System.getenv("RS_API_KEY")); byte[] image = client.take( TakeOptions.url("https://example.com").width(1280).height(800).element("p") ); Files.write(Path.of("paragraph.png"), image);
There is no official .NET SDK. C# calls the HTTP API directly with HttpClient. We ran this listing against a local stub that returns the documented response shapes, not against the live API:
using System.Net.Http.Headers; using System.Text; using System.Text.Json; var apiKey = Environment.GetEnvironmentVariable("RS_API_KEY") ?? throw new InvalidOperationException("Set RS_API_KEY"); using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) }; http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var body = JsonSerializer.Serialize(new { url = "https://example.com", viewport = new { width = 1280, height = 800 }, output = new { format = "png" }, }); var response = await http.PostAsync("https://api.renderscreenshot.com/v1/screenshot", new StringContent(body, Encoding.UTF8, "application/json")); if (!response.IsSuccessStatusCode) { using var error = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); var e = error.RootElement.GetProperty("error"); Console.Error.WriteLine($"{(int)response.StatusCode} {e.GetProperty("code")}: {e.GetProperty("message")}"); return 1; } await File.WriteAllBytesAsync("page.png", await response.Content.ReadAsByteArrayAsync()); Console.WriteLine($"CDN copy: {response.Headers.GetValues("X-Cache-URL").FirstOrDefault()}"); return 0;
We send the JSON as StringContent on purpose: PostAsJsonAsync streams the body without a Content-Length header, and some proxies and test servers reject that. The full wire format, with five more languages, is in using the API without an SDK. Waits, element capture and dark mode map to the wait and capture parameters.
Selenium is still the right tool when the screenshot is part of a browser test: a failing step, a visual check after a click, a page behind your own login flow. Use an API when the screenshot is the product. Create a free account to get 150 free credits every month and compare the output with your Selenium capture.
Related guides
- Selenium screenshots in Python: Selenium Manager, headless pitfalls and a pytest failure hook
- Selenium full page screenshots: Firefox's command, Chrome clips and stitching
- Playwright vs Selenium
- How to take website screenshots with code
Related in this topic
- How to Take Website Screenshots with Code: 6 Ways Take a screenshot of a website programmatically: DevTools, the Chrome CLI, Puppeteer, Playwright, Selenium or an API. Timed runs and code for each.
- Selenium Full Page Screenshot: Firefox, Chrome, Stitching Take a full page screenshot in Selenium: Firefox's native call, Chrome via CDP Page.captureScreenshot, or scroll and stitch. Measured limits, times and code.
- Selenium Python Screenshots: save_screenshot Done Right Take a screenshot with Selenium in Python: save_screenshot, headless Chrome, exact window size, WebP output and failure screenshots in pytest. Tested code.
- ChromeDriver: Session Not Created, Version Mismatch Fix Fix 'session not created: this version of ChromeDriver only supports Chrome version N' in Selenium 4, reproduced in Docker with a tested Python fix.
- DevToolsActivePort File Doesn't Exist: Causes and Fix DevToolsActivePort file doesn't exist in Selenium? New ChromeDriver hides it behind other messages. Reproduced on Chrome 128 to 155 in Docker, with fixes.
- Playwright Screenshots: Tested Recipes and Pitfalls Take screenshots with Playwright: page.screenshot options, deviceScaleFactor, locators, masks, measured format sizes, auth, Linux fonts and production setup.