· Updated · Tutorials · By Simon Chiu

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.

Line illustration of the RenderScreenshot mascot gluing page strips into one tall page

Selenium has a full page screenshot command only for Firefox:

import sys

from selenium import webdriver

url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
options = webdriver.FirefoxOptions()
options.add_argument('-headless')
driver = webdriver.Firefox(options=options)
try:
    driver.set_window_size(1280, 800)
    driver.get(url)
    driver.get_full_page_screenshot_as_file('full-page.png')
finally:
    driver.quit()

With Chrome, driver.save_screenshot() returns only the viewport. You have three ways to get the whole page from Chrome: call the DevTools protocol, resize the window, or scroll and stitch viewport screenshots. We ran all of them on the same pages and measured height limits, time and correctness. The short version: use Firefox's native call when you can, CDP clips when you need Chrome, and avoid stitching. For the basics of Selenium screenshots, read Selenium screenshots; for other tools, see full page screenshots compared.

Tested on 2026-10-15 with Python 3.14.8, Selenium 4.51.0, Pillow 12.3.0, Google Chrome 155.0.8059.40 (headless new) and Firefox 158.0 with geckodriver 0.37.1 (headless), on macOS 27.0.1. Times are medians of 3 runs against a local server.

Setup

pip install selenium==4.51.0 pillow==12.3.0

Selenium Manager downloads chromedriver and geckodriver for you when it can reach the internet. If your CI blocks that, install the drivers yourself and pass service=webdriver.FirefoxService(executable_path=...), which is what we did for Firefox.

The four methods on a 20,000 px page

Our test page is 20,000 px of 1,000 px colored bands with a magenta END band. A script checks every band in the output.

Method Output Correct Median time
Firefox get_full_page_screenshot_as_png() 1280 x 20,000 Yes 160 ms
Chrome CDP Page.captureScreenshot, one call 1280 x 20,000 No: repeats the top after 16,384 px 273 ms
Chrome CDP, 8,000 px clips pasted with Pillow 1280 x 20,000 Yes not timed
Chrome window resized to the page height 1280 x 19,857 Bottom 143 px missing 20,978 ms
Chrome scroll and stitch 1280 x 20,000 Yes on this page 1,846 ms
Firefox scroll and stitch 1280 x 20,000 Yes on this page 655 ms

Firefox's native call was the fastest and the simplest. The single CDP call in Chrome produced a file of the right size whose last 3,616 px showed the top of the page again. Resizing the window was the slowest by two orders of magnitude, and in headless Chrome the window size includes 143 px of browser UI, so the image ended before the page did.

Height limits

Method Largest correct capture Above it
Firefox get_full_page_screenshot_as_png 65,000 px (tested) 100000 exceeds the maximum allowed screenshot height of 65535 pixels
Chrome CDP, one call 16,384 device px Image has the right size, content repeats from the top
Chrome CDP, clips 40,000 px tested —

One detail from our runs: in one session, the single CDP call rendered 32,000 and 50,000 px pages correctly, after the window had earlier been resized to 20,000 px tall. In fresh sessions it repeated content every time: 7 captures at heights from 20,000 to 50,000 px. Do not depend on the state of the session; use clips.

Firefox: get_full_page_screenshot_as_*

Selenium's Python bindings have three variants on the Firefox driver:

driver.get_full_page_screenshot_as_file('page.png')   # writes a file, returns True or False
png = driver.get_full_page_screenshot_as_png()        # bytes
b64 = driver.get_full_page_screenshot_as_base64()     # str

They call a geckodriver extension command, so they exist only on webdriver.Firefox, not on Chrome, Edge or Safari. The width is the window's content width; set the window size before get() so the layout matches what you want.

Chrome: Page.captureScreenshot in clips

execute_cdp_cmd sends a Chrome DevTools Protocol command through chromedriver. captureBeyondViewport: true with a clip lets Chrome draw a region outside the window. To stay under the 16,384 px limit, capture 8,000 px at a time and paste the parts with Pillow:

import base64
import io
import sys

from PIL import Image
from selenium import webdriver

SEGMENT = 8000  # CSS px per capture; stays below Chrome's 16,384 px texture limit


def full_page_png(driver, path):
    width = driver.execute_script('return document.documentElement.scrollWidth')
    height = driver.execute_script('return document.documentElement.scrollHeight')
    canvas = Image.new('RGB', (width, height), 'white')
    for top in range(0, height, SEGMENT):
        clip = {'x': 0, 'y': top, 'width': width, 'height': min(SEGMENT, height - top), 'scale': 1}
        shot = driver.execute_cdp_cmd('Page.captureScreenshot', {
            'format': 'png',
            'captureBeyondViewport': True,
            'clip': clip,
        })
        canvas.paste(Image.open(io.BytesIO(base64.b64decode(shot['data']))), (0, top))
    canvas.save(path)
    return width, height


url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
options = webdriver.ChromeOptions()
options.add_argument('--headless=new')
options.add_argument('--window-size=1280,800')
driver = webdriver.Chrome(options=options)
try:
    driver.get(url)
    print(full_page_png(driver, 'full-page.png'))
finally:
    driver.quit()

On our 40,000 px page this produced all 40 bands and the END band. The code assumes a device pixel ratio of 1, which is what headless Chrome uses by default. If you emulate a higher ratio, multiply the paste position and canvas size by it, and use SEGMENT = 8000 // ratio.

execute_cdp_cmd exists on Chromium-based drivers (Chrome and Edge). It is not part of the WebDriver standard; Selenium is moving toward WebDriver BiDi, whose browsingContext.captureScreenshot command takes an origin of "document" for full-page captures. We used CDP because it is what Selenium 4.51 exposes directly in Python.

Why scroll and stitch is the last resort

Stitching takes a viewport screenshot, scrolls one screen, takes another, and pastes them together. It works in every browser, which is why many older answers recommend it. On a plain page it was correct. On our page with a fixed header and a fixed cookie bar, it produced a copy of each on every screen:

Method on the sticky page Header copies Cookie bar copies
Firefox get_full_page_screenshot_as_png 1 1, across the content where the first screen ended
Chrome CDP Page.captureScreenshot 1 1, same place
Chrome window resized to the page height 1 1, at the bottom
Chrome scroll and stitch 7 7
Firefox scroll and stitch 7 6

Chrome needed 7 screens, not 6, because headless Chrome's 800 px window has a 657 px viewport. Resizing has its own problem: sections sized with 100vh grow with the window. In our Puppeteer test of the same page, a resized viewport made the hero 4,604 px tall.

Stitching also triggers scroll-dependent effects (parallax, headers that shrink on scroll, animations on reveal) that then differ from screen to screen. If you must stitch, hide fixed elements first; the sticky header guide has the JavaScript, which you can run with driver.execute_script.

Lazy images in Selenium

No full-page method scrolls, so lazy images below the first screen stay as placeholders. Run the scroll and wait in the page before the capture. In Selenium, execute_async_script passes a callback as the last argument:

driver.execute_async_script('''
  const done = arguments[arguments.length - 1];
  (async () => {
    for (let y = 0; y < document.documentElement.scrollHeight; y += innerHeight) {
      scrollTo(0, y);
      await new Promise((r) => setTimeout(r, 150));
    }
    scrollTo(0, 0);
    await Promise.all([...document.images]
      .filter((img) => img.getAttribute('src') && !img.complete)
      .map((img) => img.decode().catch(() => {})));
  })().then(done, done);
''')

Raise the script timeout first if pages are long: driver.set_script_timeout(60). On our lazy page (20 images, 800 ms each) this took the native Firefox capture and the CDP Chrome capture from 0 loaded images to all 20. The lazy image guide measures why both the scroll and the wait are needed.

Java and other bindings

The same three approaches exist in every binding. In Java, FirefoxDriver has getFullPageScreenshotAs(OutputType.FILE), and ChromeDriver has executeCdpCommand("Page.captureScreenshot", params). The 16,384 px Chrome limit and the 65,535 px Firefox limit come from the browsers, so they apply in every language.

Troubleshooting

Symptom Cause Fix
AttributeError: ... has no attribute 'get_full_page_screenshot_as_file' You are using Chrome or Edge Use Firefox, or CDP clips
Chrome capture repeats the top of the page Single CDP call above 16,384 device px Clips of 8,000 px
exceeds the maximum allowed screenshot height of 65535 pixels Firefox limit Capture in parts with window.scrollTo plus get_screenshot_as_png, or reduce the page
Image 143 px shorter than the page Resized window includes browser UI Use CDP clips instead of resizing
Resize method takes 20 s Chrome lays out and paints a huge window Use CDP clips
Header and cookie bar on every screen Scroll and stitch Use a native method, or hide fixed elements first
Lazy images missing No scroll before the capture execute_async_script scroll and wait
Unable to obtain driver for firefox Selenium Manager could not download a driver or find Firefox Install geckodriver and pass FirefoxService(executable_path=...)
ScriptTimeoutException during the scroll Default script timeout too short driver.set_script_timeout(60)

The same capture with one API call

If Selenium is only in your stack to take screenshots, an API removes the driver management. RenderScreenshot's full_page preset scrolls for lazy content before it captures:

import os
from renderscreenshot import Client, TakeOptions

client = Client(os.environ['RS_API_KEY'])
image = client.take(TakeOptions.url('https://example.com').preset('full_page').format('png'))
with open('full-page.png', 'wb') as f:
    f.write(image)

The scroll step, delay and the max_height cap (16,000 px by default) are in the full-page API guide. Sign up for a free key to compare the output with your Selenium capture.

Related in this topic