ScreenshotNeo

BlogHow-to

How to Render HTML to PNG in Python

Render HTML as a browser-faithful PNG in Python with Playwright, including full-page, element, URL, HTML-string, and troubleshooting examples.

By the ScreenshotNeo team1 October 20267 min read

Use Playwright’s Python API when you need a browser-faithful PNG. Create or open a page, wait for the content your application needs, then call page.screenshot(path="output.png"). Add full_page=True for the complete scrollable document or call page.locator("selector").screenshot(...) for one element. Playwright can also return PNG bytes instead of writing a file. See the official screenshots guide and Page API reference.

What “render HTML to PNG” means

HTML must first be laid out by a rendering engine: CSS is applied, fonts and images are loaded, and JavaScript may modify the DOM. A browser screenshot captures the resulting pixels. This is different from serializing the HTML source or drawing a limited subset of CSS yourself.

Playwright drives Chromium, Firefox, and WebKit through one Python API. It supports viewport screenshots, full-page screenshots, element screenshots, PNG/JPEG/WebP output, and in-memory image bytes.

1. Install Playwright and a browser

Use the current commands in Playwright’s Python installation guide. A typical setup is:

python -m pip install playwright
python -m playwright install chromium

The browser download is separate from the Python package. In CI or a container, install the browser during image creation and keep the runtime environment consistent with the build environment.

2. Render an HTML string to PNG

from pathlib import Path
from playwright.sync_api import sync_playwright

html = """


  
    
    
  
  
    

Hello, PNG

Rendered by a browser.

""" with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page(viewport={"width": 1200, "height": 800}) page.set_content(html) page.screenshot(path="output.png", full_page=True) browser.close() print(Path("output.png").resolve())

set_content supplies an HTML string directly. If it references relative images, stylesheets, or scripts, use absolute URLs or provide a page origin that can resolve those assets.

3. Render a remote URL

from playwright.sync_api import sync_playwright

url = "https://example.com"

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1440, "height": 900})
    page.goto(url)
    page.screenshot(path="page.png", full_page=True)
    browser.close()

Navigation completion is not the same as application readiness. For a JavaScript application, wait for a stable selector, an explicit application signal, or the state that your page requires before taking the screenshot.

4. Choose the capture scope

Viewport screenshot

page.screenshot(path="viewport.png")

This captures the visible viewport. Set its dimensions when output size must be predictable:

page.set_viewport_size({"width": 1280, "height": 720})

Full-page screenshot

page.screenshot(path="entire-page.png", full_page=True)

full_page=True captures the full scrollable page. Very long documents create large images and may expose layout that only appears after scrolling; load lazy content before capture when required.

One element

page.locator("article .invoice").screenshot(path="invoice.png")

Prefer a stable selector such as a data attribute over a class used only for styling.

Return PNG bytes

from PIL import Image
from io import BytesIO
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.set_content("<h1>In memory</h1>")
    png_bytes = page.screenshot()  # bytes when path is omitted
    image = Image.open(BytesIO(png_bytes))
    print(image.size)
    browser.close()

This lets you send the result to object storage, an HTTP response, or an image-processing pipeline without a temporary file.

5. Screenshot options that matter

Need Playwright option or method Practical note
Entire document full_page=True Captures the scrollable page rather than only the viewport.
Specific component page.locator(selector).screenshot() Useful for cards, invoices, charts, or previews.
Format type="png", "jpeg", or "webp" PNG is lossless; JPEG quality does not apply to PNG.
Retina-sized output scale="css" or scale="device" CSS scale keeps dimensions in CSS pixels; device scale can produce higher pixel dimensions.
Transparent background omit_background=True Use where the browser and page content support transparency.
Consistent layout browser.new_page(viewport={...}) Fix width and height instead of relying on a desktop default.

For PNG output, do not add a JPEG quality setting. Check the version-specific API reference when you depend on an option that may differ between Playwright releases.

6. Wait for dynamic content

Choose a readiness condition that represents the page, rather than adding an arbitrary long delay.

page.goto("https://example.com/dashboard")
page.locator("[data-rendered='true']").wait_for()
page.screenshot(path="dashboard.png", full_page=True)

For assets that appear after JavaScript runs, wait for the relevant image, chart, or content selector. If the page intentionally streams or animates, disable animation with page-specific CSS or wait for a stable state. A screenshot taken too early can contain skeletons, missing fonts, or blank chart regions.

7. Async Python version

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page(viewport={"width": 1280, "height": 800})
        await page.goto("https://example.com")
        await page.screenshot(path="async.png", full_page=True)
        await browser.close()

asyncio.run(main())

The asynchronous API is useful when a service is already organized around an event loop. Always close the browser in a finally-equivalent lifecycle, such as the async context manager above.

8. Local files, fonts, and assets

For local HTML, use a file URL or set the content directly. Relative asset paths must resolve from a location the browser can access. Remote fonts and images can change the output or delay readiness; either host them reliably, wait for them, or inline the assets when reproducibility matters.

PNG output records pixels, not the original DOM. Keep the HTML, CSS, browser version, viewport, and important asset versions under control if you need repeatable visual output.

9. Playwright versus WeasyPrint

Playwright is the safer default when the page needs browser JavaScript, modern browser layout, or a screenshot that resembles what a user sees.

WeasyPrint is a document renderer whose current stable 70.0 documentation focuses on PDF output. Historical 52.5 documentation included write_png, but that older API should not be presented as a current solution without checking the exact version you deploy. Rendering behavior can change between versions, so verify output against your target HTML. See the current WeasyPrint API reference and the 52.5 API reference.

10. Troubleshooting

Symptom Likely cause Fix
Executable doesn't exist Browser binaries were not installed. Run the browser installation command from the Playwright guide in the same environment used at runtime.
PNG is blank or incomplete Capture happened before JavaScript or assets finished. Wait for a meaningful selector or application-ready signal.
Images are missing Relative URLs cannot resolve, requests failed, or lazy loading has not triggered. Use resolvable URLs, check network access, and load the required content before capture.
Unexpected dimensions Viewport and device scale were implicit. Set the viewport explicitly and choose the intended scale.
Element screenshot fails Selector matches nothing or the element is not visible. Use a stable selector, wait for it, and confirm it has a rendered bounding box.
Fonts differ between machines Font availability or loading timing differs. Install or serve the same fonts and wait for the page’s font-dependent content.
Full-page image is enormous The document is very long or has wide overflow. Capture a component, constrain layout overflow, or split the document into logical sections.
Output changes between runs Animations, timers, ads, remote data, or browser versions vary. Freeze dynamic inputs where possible, wait for stable state, and pin the browser/runtime version.

11. Performance, reliability, and cost

  • Reuse responsibly: launching a browser is heavier than opening a page. A long-running worker can reuse a browser while creating isolated pages for jobs.
  • Control concurrency: too many simultaneous pages increase memory use and can make captures less reliable. Bound the queue and measure your own workload.
  • Reduce work: capture an element when a full document is unnecessary, avoid loading irrelevant resources, and use a fixed viewport.
  • Set timeouts: define navigation and readiness timeouts, then record the URL and failure reason so jobs can be retried deliberately.
  • Retry selectively: retry transient navigation or network failures, but investigate deterministic selector and layout errors instead of retrying indefinitely.
  • Budget infrastructure: self-hosted Playwright costs come from browser CPU, memory, storage, and engineering time. The dossier contains no benchmark, so size capacity from your own pages and concurrency.

Or skip the browser setup

ScreenshotNeo provides a hosted screenshot API. One GET request returns PNG, JPEG, WebP, or PDF, while its browser workflow accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed. Every plan includes full-page and element capture, custom CSS and JavaScript, waiting controls, blocking rules, headers, cookies, user agents, device presets, retina scale, caching, async jobs, bulk capture, and an MCP server for AI agents.

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

See the ScreenshotNeo API documentation for options and response headers. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. An MCP server lets Claude, Cursor, and other MCP clients call screenshot tools directly.

Create a free ScreenshotNeo account and start with the 1,000 monthly screenshots.

FAQ

Can Python render HTML without a browser?

Yes, document renderers exist, but the right choice depends on CSS and JavaScript requirements. For browser behavior and browser-like pixels, use Playwright.

Should I use a full-page screenshot for every page?

No. Use a viewport or element screenshot when that matches the output you need; full-page captures can be very large.

Can I process the PNG without saving it?

Yes. Omit the screenshot path and pass the returned bytes to an image library or response stream.

Why is a screenshot different in CI?

Fonts, browser versions, device scale, animations, remote assets, and data timing can differ. Pin the runtime and wait for a stable page state.