DEV Community

Dan E
Dan E

Posted on • Originally published at rendex.dev

HTML Table to PNG in Python Without wkhtmltoimage

You have an HTML table (a financial summary, a styled DataFrame, a report section your frontend already renders) and you need it as a PNG from a Python backend. For a decade the reflex answer was wkhtmltoimage (via imgkit) or a full Selenium driver. Both still technically run in 2026, and both are the wrong place to start a new project: the wkhtmltoimage engine was archived in January 2023, and Selenium drags a whole browser fleet behind one PNG.

This is the opinionated, decision-first version. You have a table, you want a PNG (and probably a PDF), and you do not want a frozen WebKit binary or a Chromedriver version-match problem in your deploy. There are exactly two paths worth your time: call a rendering API, or self-host Playwright. For the full six-way survey with every library benchmarked, see Render an HTML Table to PNG in Python (5 Ways). This post picks the winners and skips the legacy detours.

Path 1: a rendering API (nothing to host)

POST the HTML, get PNG bytes back. A real, current Chromium runs on Cloudflare's edge through Rendex, so modern CSS (Flexbox, Grid, web fonts) renders exactly as it does in Chrome, and there is no browser to install, patch, or keep alive in your image. The rendex Python SDK wraps the call in two lines.

# pip install rendex   (key from rendex.dev/login)
import os
from pathlib import Path
from rendex import Rendex

rendex = Rendex(os.environ["RENDEX_API_KEY"])

table_html = """
<table style="border-collapse:collapse;font-family:system-ui;font-size:14px">
  <thead>
    <tr style="background:#f1f5f9">
      <th style="text-align:left;padding:8px 14px">Line item</th>
      <th style="text-align:right;padding:8px 14px">Amount</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding:8px 14px">Subscriptions</td>
        <td style="text-align:right;padding:8px 14px">€482,100</td></tr>
    <tr><td style="padding:8px 14px">Professional services</td>
        <td style="text-align:right;padding:8px 14px">€91,400</td></tr>
  </tbody>
</table>
"""

result = rendex.render_html(table_html, format="png", device_scale_factor=2)
Path("table.png").write_bytes(result.image)
print("Rendered", result.metadata)
Enter fullscreen mode Exit fullscreen mode

Because the table is the only thing on the page, render at device_scale_factor=2 for a crisp 2x PNG, and add full_page=True when a long report runs past the viewport. The SDK posts to /v1/screenshot under the hood and hands you the image bytes; if you would rather skip the dependency, the same render over raw HTTP returns those bytes straight to a file:

curl -X POST https://api.rendex.dev/v1/screenshot \
  -H "Authorization: Bearer $RENDEX_API_KEY" \
  -H "Content-Type: application/json" \
  --output table.png \
  -d '{
    "html": "<table style=\"border-collapse:collapse;font-family:system-ui\"><tr><th>Line item</th><th>Amount</th></tr><tr><td>Subscriptions</td><td>€482,100</td></tr></table>",
    "format": "png",
    "deviceScaleFactor": 2,
    "fullPage": true
  }'
Enter fullscreen mode Exit fullscreen mode

That is the whole pipeline. It deploys to a serverless function or a slim container with no Chromium binary in the image, which is the part that usually breaks at 2am. The free tier is 100 renders/month, no card, so you can prove the report flow before you pay for anything. The browser-based test of the same request lives in the HTML-to-image playground.

Same call, now a PDF

Most report pipelines need the table as a PNG for a dashboard and as a PDF for an emailed deliverable. It is the same request with one field changed, so you do not bolt on a second library or a PDF-to-PNG conversion step.

# Same html, format="pdf" → PDF bytes back
from pathlib import Path
from rendex import Rendex
import os

rendex = Rendex(os.environ["RENDEX_API_KEY"])

pdf = rendex.render_html(table_html, format="pdf").image
Path("table.pdf").write_bytes(pdf)
Enter fullscreen mode Exit fullscreen mode

Rendex renders to PNG, JPEG, WebP, or PDF from the same endpoint. For the REST field reference (every option, every output format), see the API reference; the three-minute setup is in the quickstart.

Path 2: self-host Playwright

If the HTML cannot leave your network, or you already run a browser for other jobs, Playwright is the modern self-hosted answer. It ships evergreen bundled browsers, so there is no Chromedriver to version-match. set_content() loads the HTML, then you screenshot the table element directly.

# pip install playwright  &&  playwright install chromium
from playwright.sync_api import sync_playwright

table_html = "<table border=1><tr><th>Line item</th><th>Amount</th></tr>" \
             "<tr><td>Subscriptions</td><td>€482,100</td></tr></table>"

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(device_scale_factor=2)
    page.set_content(table_html)
    page.locator("table").screenshot(path="table.png")  # crop to the table
    browser.close()
Enter fullscreen mode Exit fullscreen mode

It works and you keep every byte local. The cost is ownership: the bundled browsers add hundreds of MB to your image, you patch them on Playwright's cadence, and a long-running process leaks memory and leaves zombie processes if you forget to close contexts. That is a fair trade when data residency is the constraint, and a poor one when rendering a table is a side-task to your actual report logic.

What to skip, and why

The legacy options are not broken so much as retired. Reach for them only to keep an existing script breathing.

  • wkhtmltoimage / imgkit: the engine was archived in January 2023 (last stable release June 2020). It is a frozen old WebKit, so modern CSS Flex/Grid, web fonts, and emoji are broken or silently dropped, and a headless box throws QXcbConnection: Could not connect to display unless you wrap every call in xvfb-run. Fine for a legacy report; not a 2026 starting point.
  • Selenium: maintained and battle-tested, but it is the most boilerplate for one PNG, full-page capture is not first-class, and you still own the browser fleet, same as Playwright with more ceremony.
  • WeasyPrint: an excellent pure-Python engine, but it renders to PDF only (PNG export was removed in v53) and runs no JavaScript. Great for static HTML-to-PDF, useless when you need a PNG or your table is built by JS.

The honest, side-by-side trade-offs for all five live in the 5-ways comparison, and the hosted contenders are scored in best HTML-to-image APIs 2026.

The decision in one line

If you can send the HTML out, call the API: no browser to host, PNG or PDF from one request, and your deploy stays a plain Python service. If the HTML must stay in-network, self-host Playwright and accept that you now operate a browser. Either way, the answer to "HTML table to PNG in Python" in 2026 is not wkhtmltoimage.

The fastest way to see the API path is to render your own table: paste it into the playground, then grab a free key and move the same call into your report job. 100 renders a month, no card, enough to ship the pipeline before you scale it.

Top comments (0)