Free tools Windows power users keep installed

One-click scans. No signup required.

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

The most reliable way to convert an HTML table to an image in Python is to render the table in a real browser, then capture either the table element or the complete page with Playwright. Browser rendering preserves CSS, fonts, column sizing, borders, wrapping, and responsive behavior; parsing HTML and drawing cells yourself usually does not.

This guide shows a complete local workflow, including pandas-generated tables, asynchronous content, PNG/JPEG/WebP output, full-page captures, high-density scaling, transparency, scrollable containers, troubleshooting, and an API alternative.

Install Python and Playwright

Use a current Python 3 environment and install Playwright:

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

The second command downloads the Chromium browser used by Playwright. Run it once for each environment (virtual environment, CI image, or container) in which you capture tables.

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

Convert an existing HTML table to PNG

This synchronous example writes only the rendered <table> to table.png:

from playwright.sync_api import sync_playwright

html = """
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { margin: 24px; font-family: Arial, sans-serif; }
    table { border-collapse: collapse; width: 420px; }
    th, td { border: 1px solid #cbd5e1; padding: 8px 10px; text-align: left; }
    th { background: #0f172a; color: white; }
    tr:nth-child(even) { background: #f8fafc; }
  </style>
</head>
<body>
  <table id="sales">
    <thead><tr><th>Fruit</th><th>Count</th></tr></thead>
    <tbody><tr><td>Apples</td><td>12</td></tr></tbody>
  </table>
</body>
</html>
"""

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 900, "height": 700})
    page.set_content(html, wait_until="load")
    page.locator("#sales").screenshot(path="table.png")
    browser.close()

Playwright’s screenshot API supports locator captures and PNG, JPEG, and WebP output. A locator screenshot follows the element’s rendered bounding box, so margins outside the table are not included.

Capture the whole page instead

Use a page screenshot when the table needs its title, explanatory text, or surrounding layout:

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

full_page=True creates a tall image covering the page’s full scrollable area. It is different from a viewport screenshot, which includes only what is currently visible.

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

Generate the table from pandas

DataFrame.to_html() creates table markup, while Styler.to_html() adds CSS and formatting. The browser must receive the resulting HTML before the screenshot is taken.

import pandas as pd
from playwright.sync_api import sync_playwright

df = pd.DataFrame({
    "Product": ["Keyboard", "Mouse", "Monitor"],
    "Units": [18, 42, 7],
    "Revenue": [1299.50, 840.00, 2100.00],
})

# Plain table markup:
table_html = df.to_html(index=False, classes="report")

# For CSS-aware formatting, use this instead:
# table_html = df.style.format({"Revenue": "${:,.2f}"}).to_html()

html = f"""

{table_html}"""

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1000, "height": 800})
    page.set_content(html, wait_until="load")
    page.locator("table.report").screenshot(path="pandas-table.png", type="png")
    browser.close()

Use df.to_html() when the DataFrame’s values are enough. Use df.style.to_html() when you need number formats, conditional colors, gradients, or other Styler-generated CSS. pandas documents both the DataFrame HTML writer and Styler’s HTML/CSS output (DataFrame HTML documentation; Styler API).

Choose the image scope and format

Requirement Playwright choice Result
Only one table page.locator("table").screenshot(...) Element-sized image
One specific table Use an ID, class, or CSS selector Avoids capturing other tables
Entire document page.screenshot(full_page=True) Full scrollable page
PNG type="png" or omit type Lossless default; quality is not used
JPEG type="jpeg", quality=85 Smaller, opaque image; quality is 0–100
WebP type="webp", quality=90 Modern compressed format
More pixels scale="device" Device-pixel output instead of CSS-pixel sizing

Playwright also supports clipping, background handling, and screenshot bytes. To upload the image without creating a temporary file, omit path:

image_bytes = page.locator("table").screenshot(type="webp", quality=90)
with open("table.webp", "wb") as f:
    f.write(image_bytes)

Transparent backgrounds are available for page screenshots when the background is omitted, but JPEG cannot represent transparency. Use PNG or WebP when transparent output matters.

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

Wait for data, fonts, and remote styles

Capture only after the content that determines the table’s appearance is ready. For a table populated by JavaScript, wait for a selector or a meaningful row:

page.goto("https://example.com/report", wait_until="domcontentloaded")
page.locator("table#report tbody tr").first.wait_for(state="visible")
page.locator("table#report").screenshot(path="report.png")

If the page uses a known loading indicator, wait for it to disappear. If a web font changes column widths, wait for fonts before capture:

page.wait_for_function("document.fonts && document.fonts.status === 'loaded'")

A fixed delay such as page.wait_for_timeout(1000) can be a fallback, but a selector or state-based wait is less fragile. Playwright’s documentation describes the screenshot options and loading behavior; its Page and Locator APIs cover the relevant wait methods.

Handle tables inside scrolling containers

A locator screenshot captures the element’s rendered box. If a table is inside a container with overflow: auto, the visible scrollport may exclude rows that are outside it. Prefer a layout in which the table grows to its full height before capture:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
page.locator("#table-wrapper").evaluate("el => { el.style.overflow = 'visible'; el.style.height = 'auto'; }")
page.locator("#table-wrapper table").screenshot(path="all-rows.png")

For virtualized tables, rows not currently rendered in the DOM cannot be photographed by any screenshot call. Disable virtualization or export a non-virtualized print view first. For a horizontally clipped table, increase the viewport or remove the container’s horizontal clipping before capturing.

Control dimensions, scaling, and layout

Set a deterministic viewport

Responsive CSS can produce different images on different machines. Set the viewport explicitly:

page = browser.new_page(viewport={"width": 1400, "height": 900}, device_scale_factor=1)

Use a larger width to prevent unwanted wrapping. Use device_scale_factor=2 or scale="device" when a higher-density image is required; the file will contain more pixels and may be larger.

Inject capture-only CSS

Hide controls, constrain widths, or set print colors without changing your source page:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
page.add_style_tag(content="""
  .export-button, .cookie-banner { display: none !important; }
  table { break-inside: avoid; }
  * { print-color-adjust: exact; }
""")

Keep table content accessible and semantic in the HTML; capture-time CSS should change presentation, not data.

Save a screenshot for an existing web URL

For a remote page, navigate first and then select the table:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1280, "height": 900})
    page.goto("https://example.com/report", wait_until="networkidle")
    page.locator("table#report").screenshot(path="report.png")
    browser.close()

networkidle can be unsuitable for pages with long-lived analytics or streaming requests. In that case, use domcontentloaded and wait for the table’s actual ready condition.

Common errors and fixes

  • “Executable doesn’t exist” or browser launch failure: run python -m playwright install chromium in the same environment as your script.
  • “Locator resolved to hidden or empty content”: verify the selector, wait for the table, and check that JavaScript has inserted rows.
  • Missing CSS or images: use an absolute URL for remote assets, allow the page to finish loading, and wait for the relevant selector or font.
  • Screenshot is cropped: capture the table locator rather than a parent with fixed dimensions; remove clipping and overflow rules when all rows are required.
  • Text wraps differently in CI: set the viewport, install the same fonts, and use a fixed device scale factor.
  • Blank image: inspect the HTML with page.locator("table").count() and save page.content() for debugging before capture.
  • JPEG has an unexpected background: JPEG is opaque; choose PNG or WebP for transparency.
  • Very tall image fails or is unwieldy: capture the table in sections, create a print/PDF layout, or reduce unnecessary row spacing and pixel scale.

Performance, reliability, and repeatability

  • Reuse one browser process for multiple tables or URLs, creating a new page per capture.
  • Set explicit timeouts and close pages in a finally block in production jobs.
  • Prefer selector-based waits over arbitrary sleeps.
  • Use PNG for archival fidelity, WebP for smaller modern assets, and JPEG for photographic pages where minor compression is acceptable.
  • Record the viewport, browser version, CSS, and input data when image diffs must be reproducible.
  • For untrusted HTML, isolate the browser context and avoid granting unnecessary file or network access.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. It renders a URL remotely, removes cookie banners, newsletter popups, and chat widgets before capture, and bills only clean shots: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. Its response identifies the result with X-Page-Verdict and X-Billed headers.

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

For a table hosted at a public URL, one GET request returns an image. The API accepts PNG, JPEG, or WebP and supports full-page or element capture, custom CSS and JavaScript, waits, device presets, retina scale, headers, cookies, authentication, and other capture controls. See the ScreenshotNeo API documentation.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/report -o report.webp

Python

import requests

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

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/report' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const body = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('report.webp', body);

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every feature is on every plan: 1,000 shots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Which method should you use?

Situation Best choice
HTML or pandas data is generated in your Python process Playwright with set_content()
You need the exact table element and local CSS Playwright locator screenshot
You need surrounding headings and page context Playwright full-page screenshot
The page is public and you do not want browser installation or maintenance ScreenshotNeo API
An AI agent must perform captures ScreenshotNeo MCP server

Frequently Asked Questions

Can I convert an HTML table without a browser?

You can redraw cells with a graphics library, but that is a different rendering system and will not automatically reproduce browser CSS. Use Playwright when browser fidelity matters.

Can Playwright capture only one table on a page?

Yes. Target it with a locator such as page.locator("#sales") and call screenshot().

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

Why are some rows missing from my image?

The table may be inside a scrollable or virtualized container. Make the full table render and remove clipping before capture.

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.