Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Capture the browser window with Selenium, then use Pillow to draw text on the saved PNG. Selenium’s save_screenshot() saves the current window; Pillow’s ImageDraw adds the label to the image. This post-capture method changes the image file, not the page in the browser.

What you need

The workflow uses Selenium to capture a PNG and Pillow to edit it. You need a configured Selenium WebDriver, a page loaded in its browser, and Pillow available in the same Python environment as your script. The example below starts a Chrome WebDriver; it assumes Chrome and the dependencies required by your Selenium setup are available. If your project already creates a driver, use that driver instead of creating another one.

  • Selenium: supplies the browser window screenshot.
  • Pillow: opens the PNG, draws text, and saves the edited image.
  • Two output paths: one for the original capture and another for the annotated copy, so the unmodified screenshot remains available.

Selenium’s Python API also offers get_screenshot_as_png() for screenshot bytes. For a simple file-based annotation, save_screenshot() keeps the workflow straightforward.

Capture a screenshot and add a label

Load the target page, save the current browser window as a PNG, open that file with Pillow, and draw the label at the coordinates you choose. The following example uses a fixed label and top-left position; change the URL, wording, coordinates, and color for your page.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from pathlib import Path

from PIL import Image, ImageDraw
from selenium import webdriver

screenshot_path = Path("screenshot.png")
annotated_path = Path("screenshot_annotated.png")

# Create a browser driver, or replace this with your project's existing driver.
driver = webdriver.Chrome()

try:
    driver.get("https://example.com")

    # Selenium saves the current window as a PNG.
    if not driver.save_screenshot(str(screenshot_path)):
        raise OSError(f"Could not save screenshot to {screenshot_path}")

    # Open the saved image and draw the label onto it.
    with Image.open(screenshot_path) as source:
        image = source.copy()

    draw = ImageDraw.Draw(image)
    draw.text((20, 20), "Example page", fill="red")
    image.save(annotated_path)
finally:
    driver.quit()

print(f"Original screenshot: {screenshot_path}")
print(f"Annotated screenshot: {annotated_path}")

Run the script in the environment where Selenium, Pillow, and the browser driver are available. The original image is saved as screenshot.png; the edited version is screenshot_annotated.png. If your own application already manages the browser lifecycle, keep that lifecycle management there rather than quitting a shared driver prematurely.

Why check the screenshot return value?

save_screenshot() returns False if an I/O error occurs. Checking it before opening the file prevents the annotation step from proceeding as if a valid capture had been written. Raising an error makes the failure visible to the caller rather than silently producing an absent or stale output.

Using your existing WebDriver

In a test suite or browser automation program, the driver is commonly created elsewhere and may already be on the page you want to capture. In that case, keep the capture-and-draw steps and omit the example’s webdriver.Chrome(), driver.get(), and driver.quit() where your program already handles them. The important requirement is that the driver be on the intended page when save_screenshot() runs.

Place and format the text

Pillow’s drawing coordinates use the upper-left corner of the image as (0, 0). In draw.text((x, y), ...), the coordinates specify the text anchor; the default horizontal anchor is top-left. For example, (20, 20) starts the label 20 pixels from the left and 20 pixels from the top. Drawing outside the image bounds is discarded, so choose coordinates within the screenshot.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose coordinates from the image dimensions

Use the image’s actual dimensions when placing labels rather than assuming every browser window produces the same size. Pillow exposes dimensions as image.width and image.height. For a label near the upper-right area, one simple position is based on the width, such as (image.width - 220, 20); adjust the offset to suit the label and available space. This is only a position calculation, not automatic text fitting: a long label can still extend past an edge or cover page content.

Leave a margin around the label and inspect the resulting image, especially if the screenshot dimensions can vary. Avoid placing important text over the page content you intend the screenshot to document. The drawing call changes the image in place, so subsequent drawing operations apply to the same image object.

Choose a font

The font argument controls text typography. The short example omits it for simplicity; when predictable typography matters, pass an explicit font rather than relying on a default. A font file must be available to the environment running the script. Because font locations differ across operating systems and deployments, provide a path that exists in your own environment instead of copying a machine-specific path into portable code.

Draw multiple lines

Use ImageDraw.multiline_text() when the label contains line breaks. It supports spacing and alignment options, which are useful when several lines should read as one label. For instance:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
draw.multiline_text(
    (20, 20),
    "Checkout pagenPayment step",
    fill="red",
    spacing=4,
    align="left",
)

As with a single line, the supplied coordinates are the anchor location. Check the saved result: text that does not fit within the image bounds will be clipped by the image boundary.

Decide whether to annotate the image or the page

Post-processing with Pillow is the right choice when the text is a label for the saved evidence image—for example, a note that identifies a test case or calls attention to a region. Selenium captures the page first, and Pillow adds the label afterward. The label therefore belongs to the output image, not to the web page’s DOM or its browser state.

If the text must be part of the page before the screenshot is captured, post-processing is not equivalent. The two methods represent different things: an image-only annotation records your added label on the evidence, whereas a pre-capture page modification represents browser content at capture time. Choose based on what the screenshot is meant to show.

Common problems and fixes

Symptom Likely cause What to do
The script raises “Could not save screenshot” Selenium reported an I/O error while saving the PNG. Check that the output directory exists and the process can write to the path. Use an explicit writable path, then check the return value again.
Pillow cannot open the screenshot The capture did not produce a readable file, or the path passed to Image.open() differs from the save path. Confirm save_screenshot() returned true and that both operations use the same path. Do not attempt to open the file after a failed save.
The text is missing or partly cut off The anchor may be outside the image, or the text may extend beyond an edge. Check the image dimensions and move the anchor inside the image with enough margin for the full label. Coordinates outside the image are discarded.
The label covers important content The chosen position overlaps the page area you need to preserve. Move the text to a less important area, or choose a different annotation position after inspecting the screenshot.
The text does not look consistent between environments The script does not specify a font, or the chosen font file is unavailable in one environment. Pass an explicit font and ensure its file is available wherever the script runs.
The saved page image does not contain the annotation The original screenshot was opened or shared instead of the annotated output. Use screenshot_annotated.png as the final artifact; screenshot.png is intentionally preserved as the original.
The screenshot shows the wrong page The driver was not on the intended page when the capture call ran. Load or navigate to the target page before calling save_screenshot(), and confirm the active driver is the one used for that page.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability, and output choices

This method writes a screenshot to disk, reads it back with Pillow, and writes a second image. Keeping distinct source and output files costs an additional image file, but it preserves the unannotated capture for audit or reuse. If storage is more important than retaining the original, you can save the edited image to the original path after opening it; use a separate path while developing or when the original is evidence you may need to inspect.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Selenium API call in this workflow captures the current window as a PNG. Pillow edits that captured image; it does not change what Selenium captured. A screenshot can therefore be correct as an image operation while still being the wrong page or browser window for your purpose. Make sure the active driver is positioned as intended before capture, and verify the generated files in the same environment where the script writes them.

For an image output, save to a filename with a matching image extension such as .png. A PDF is a different output format and is not produced by this Pillow drawing example. For multiple labels, create the drawing context once, call text() or multiline_text() for each label, and save after all drawing is complete.

Or skip the browser setup

If your goal is to obtain a website screenshot rather than run a local Selenium browser, ScreenshotNeo provides a screenshot API. Its response can be a PNG, JPEG, WebP, or PDF; clean-up options can accept consent banners and remove known consent platforms, newsletter popups, and chat widgets before capture. Those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers indicate the page verdict and billing status. ScreenshotNeo also offers an MCP server with screenshot, page-info, and PDF-capture tools for AI agents. See the ScreenshotNeo website and API documentation.

Here is a Python request that writes a WebP screenshot:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

Replace YOUR_API_KEY with your ScreenshotNeo API key. The endpoint is a GET request; the response body is written to shot.webp.

Equivalent cURL and Node.js calls

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo’s free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan. Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.