Free tools Windows power users keep installed
One-click scans. No signup required.
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.
#1 Best Overall
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.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesGenerate 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.
Rank #2
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.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →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:
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →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:
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 chromiumin 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 savepage.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
finallyblock 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.
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.
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.
Best Value
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().
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.
Quick Recap
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.

