<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dan E</title>
    <description>The latest articles on DEV Community by Dan E (@danelliot).</description>
    <link>https://dev.to/danelliot</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3868746%2F528fa19c-77de-4c58-9929-e517022c67e3.jpeg</url>
      <title>DEV Community: Dan E</title>
      <link>https://dev.to/danelliot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danelliot"/>
    <language>en</language>
    <item>
      <title>Render an HTML Table to PNG in Python (5 Ways, 2026)</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Sun, 07 Jun 2026 17:15:07 +0000</pubDate>
      <link>https://dev.to/danelliot/render-an-html-table-to-png-in-python-5-ways-2026-1h5n</link>
      <guid>https://dev.to/danelliot/render-an-html-table-to-png-in-python-5-ways-2026-1h5n</guid>
      <description>&lt;p&gt;You have an HTML table — a financial summary, a report section, a styled DataFrame, a chart your frontend already renders — and you need it as a PNG from a Python backend. A cron job that emails a nightly report, an invoice rasterized into a PDF, a certificate generated per customer. The HTML is the easy part; turning it into a pixel-accurate image is where the options get messy.&lt;/p&gt;

&lt;p&gt;This guide covers five working approaches, from a single API call to a headless browser you host yourself, plus an honest note on where each one bites. Every snippet renders the same little table so you can compare them directly. (All facts verified June 2026 — wkhtmltopdf is now archived, which changes the usual advice.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Rendex API (no browser to host)
&lt;/h2&gt;

&lt;p&gt;The fastest path, and the one that needs nothing on your machine. POST the HTML to the &lt;a href="https://rendex.dev/use-cases/server-side-report-rendering" rel="noopener noreferrer"&gt;Rendex rendering API&lt;/a&gt; and get PNG bytes back. A real, modern Chromium runs on Cloudflare's edge — you don't install, patch, or keep a browser alive, and modern CSS (Flexbox, Grid, web fonts) renders exactly as it would in Chrome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install rendex
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rendex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;table style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;border-collapse:collapse;font-family:system-ui;font-size:14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
  &amp;lt;thead&amp;gt;
    &amp;lt;tr style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;background:#f1f5f9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
      &amp;lt;th style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:left;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Line item&amp;lt;/th&amp;gt;
      &amp;lt;th style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:right;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Amount&amp;lt;/th&amp;gt;
    &amp;lt;/tr&amp;gt;
  &amp;lt;/thead&amp;gt;
  &amp;lt;tbody&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Subscriptions&amp;lt;/td&amp;gt;
        &amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:right;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Professional services&amp;lt;/td&amp;gt;
        &amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:right;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;€91,400&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
  &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_scale_factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rendered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the table is the only thing on the page, render at &lt;code&gt;device_scale_factor=2&lt;/code&gt; for a crisp 2× image, and add &lt;code&gt;full_page=True&lt;/code&gt; when a long table runs past the viewport. No SDK? The same call over raw HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table.png &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "html": "&amp;lt;table style=\"border-collapse:collapse;font-family:system-ui\"&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;",
    "format": "png",
    "deviceScaleFactor": 2,
    "fullPage": true
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; production report pipelines, anything you deploy to a serverless function or a slim container, and any time you don't want a Chromium binary in your image. The free tier is &lt;strong&gt;500 renders/month, no card&lt;/strong&gt; — &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;grab a key&lt;/a&gt;. For the full report-to-PDF-deliverable pattern (templating with a &lt;code&gt;data{}&lt;/code&gt; object, embedding the PNG in a generated PDF), see the &lt;a href="https://rendex.dev/recipes/html-financial-report-to-png-python" rel="noopener noreferrer"&gt;Python report-rendering recipe&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off, honestly:&lt;/strong&gt; it's a network call and a per-render cost, and the HTML leaves your box. For a sync render Rendex processes it transiently and doesn't store it; if that's a dealbreaker for sensitive data, one of the self-hosted options below keeps everything local.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: html2image (lightweight, but you supply Chrome)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/vgalin/html2image" rel="noopener noreferrer"&gt;html2image&lt;/a&gt; is a thin wrapper around the headless mode of a Chrome/Chromium/Edge that is &lt;em&gt;already installed on the machine&lt;/em&gt; — it doesn't bundle one. Maintained (latest 2.0.7, May 2025), MIT, and genuinely the simplest local route when a browser is already present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install html2image   (Chrome/Chromium/Edge must already be installed)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;html2image&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Html2Image&lt;/span&gt;

&lt;span class="n"&gt;hti&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Html2Image&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;table border=1&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;hti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_str&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save_as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; you own keeping that Chrome patched and version-matched across every machine and CI runner, and fonts/emoji are whatever the host browser ships — Linux base images often lack CJK and color-emoji fonts, so you'll install font packages to avoid tofu boxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Playwright (the modern self-hosted default)
&lt;/h2&gt;

&lt;p&gt;If you're going to run a browser yourself, &lt;a href="https://playwright.dev/python/docs/screenshots" rel="noopener noreferrer"&gt;Playwright for Python&lt;/a&gt; is the current standard (1.60, May 2026, with evergreen bundled browsers). &lt;code&gt;set_content()&lt;/code&gt; loads your HTML; locate the table and screenshot just that element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install playwright  &amp;amp;&amp;amp;  playwright install chromium
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;table border=1&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# crop to the table
&lt;/span&gt;    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; the bundled browsers are hundreds of MB in your image; you patch them; and a long-running process leaks memory and leaves zombie processes if you don't close contexts. Full-page shots over ~16,384px can OOM. Great control — you just operate the browser fleet now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 4: imgkit / wkhtmltoimage (works, but the engine is archived)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/jarrekk/imgkit" rel="noopener noreferrer"&gt;imgkit&lt;/a&gt; wraps the &lt;code&gt;wkhtmltoimage&lt;/code&gt; binary. It's the classic answer to this question, and the one to be most careful with in 2026.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install imgkit   +   system binary: apt-get install wkhtmltopdf
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;imgkit&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;table border=1&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;imgkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# headless box: wrap in xvfb-run
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; the underlying &lt;code&gt;wkhtmltoimage&lt;/code&gt;/wkhtmltopdf engine was &lt;strong&gt;archived in January 2023&lt;/strong&gt; (last stable release June 2020) and gets no security patches. It's a frozen, old WebKit: modern CSS Flex/Grid, web fonts, and emoji are broken or silently dropped, and on a headless server it throws &lt;code&gt;QXcbConnection: Could not connect to display&lt;/code&gt; unless you wrap every call in &lt;code&gt;xvfb-run&lt;/code&gt;. imgkit itself is effectively unmaintained (last release 1.2.3). Fine for a legacy script; not what you'd start a new project on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 5: Selenium (works, heaviest for one PNG)
&lt;/h2&gt;

&lt;p&gt;Selenium drives a real browser and can screenshot an element. It's maintained and battle-tested, but it's the most boilerplate for "HTML string → one PNG."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install selenium   (Selenium Manager fetches a matching ChromeDriver)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;table border=1&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--headless=new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data:text/html;charset=utf-8,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; the classic &lt;code&gt;SessionNotCreatedException&lt;/code&gt; when ChromeDriver and the host Chrome drift apart (Selenium Manager mostly handles this now), full-page screenshots aren't first-class, and it's the same browser-fleet ownership as Playwright with more ceremony.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on WeasyPrint
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://weasyprint.org/" rel="noopener noreferrer"&gt;WeasyPrint&lt;/a&gt; comes up in every "HTML to image" search, so it's worth being precise: it's an excellent, actively-maintained pure-Python engine — but it renders to &lt;strong&gt;PDF only&lt;/strong&gt; (PNG export was removed in v53) and &lt;strong&gt;runs no JavaScript&lt;/strong&gt;. If your table is static HTML/CSS and you actually want a PDF, WeasyPrint is a great, dependency-light choice and you don't need any of the above. If you need a &lt;strong&gt;PNG&lt;/strong&gt;, or your table/chart is built by JavaScript, it can't do the job without a second PDF→PNG conversion step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you use?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Browser to host?&lt;/th&gt;
&lt;th&gt;PNG?&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rendex API&lt;/td&gt;
&lt;td&gt;No (runs on the edge)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Production pipelines, serverless, no infra to own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;html2image&lt;/td&gt;
&lt;td&gt;Yes (you supply Chrome)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Quick local jobs where a browser is already present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;Yes (bundled)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Full browser control, if you want to run the fleet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;imgkit / wkhtmltoimage&lt;/td&gt;
&lt;td&gt;No (archived engine)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Legacy scripts only — engine archived 2023&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selenium&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (viewport)&lt;/td&gt;
&lt;td&gt;Teams already standardized on Selenium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WeasyPrint&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No (PDF only)&lt;/td&gt;
&lt;td&gt;Static HTML → PDF, no JS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest summary: the self-hosted libraries all &lt;em&gt;work&lt;/em&gt; — the real question is whether you want to own a browser (install, patch, scale, debug fonts, watch memory) for what is often a side-task to your actual report logic. If yes, Playwright is the modern pick. If you'd rather keep your deployment a plain Python service and POST HTML to get a PNG back, that's exactly what &lt;a href="https://rendex.dev/use-cases/server-side-report-rendering" rel="noopener noreferrer"&gt;Rendex&lt;/a&gt; is for — 500 free renders a month to prove the pipeline before you scale.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Render a Chart to an Image on the Server (Node + Python, 2026)</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Sat, 06 Jun 2026 17:12:31 +0000</pubDate>
      <link>https://dev.to/danelliot/render-a-chart-to-an-image-on-the-server-node-python-2026-19mf</link>
      <guid>https://dev.to/danelliot/render-a-chart-to-an-image-on-the-server-node-python-2026-19mf</guid>
      <description>&lt;p&gt;You need a chart as a static image on the server — to attach to an email, embed in a generated PDF, post to Slack, or drop into a nightly report. Not an interactive widget in a browser; a PNG, generated headless, no human watching.&lt;/p&gt;

&lt;p&gt;Here's the thing nobody says up front: &lt;strong&gt;almost every server-side "chart to image" route eventually drags in one of two heavy dependencies&lt;/strong&gt; — a C/C++ canvas stack (Cairo/Skia via node-canvas) or a headless Chrome. The choice is really &lt;em&gt;which one you want to own&lt;/em&gt;. This guide walks the honest options across Node and Python (verified June 2026, including the kaleido and orca changes that broke a lot of old tutorials), then the route that moves the browser off your box entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Render the chart's HTML with an API (no canvas, no Chrome to host)
&lt;/h2&gt;

&lt;p&gt;If your chart can be expressed as HTML — a Chart.js &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, an ECharts container, an SVG, or even a styled HTML/CSS bar chart — you can POST that HTML to the &lt;a href="https://rendex.dev/use-cases/server-side-report-rendering" rel="noopener noreferrer"&gt;Rendex rendering API&lt;/a&gt; and get a PNG back. Rendex runs a real, modern headless browser on the edge, so the chart library's JavaScript actually executes; you don't install node-canvas, compile native modules, or keep a Chrome alive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install rendex
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rendex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# A self-contained HTML page with Chart.js from a CDN. animation:false so the
# chart is fully drawn by the time Rendex captures it.
&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;!doctype html&amp;gt;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;
  &amp;lt;script src=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://cdn.jsdelivr.net/npm/chart.js&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;&amp;lt;body style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;margin:0;width:800px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
  &amp;lt;canvas id=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; width=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;800&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; height=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;400&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&amp;lt;/canvas&amp;gt;
  &amp;lt;script&amp;gt;
    new Chart(document.getElementById(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;c&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;), {
      type: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,
      data: {
        labels: [&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Q1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Q2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Q3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Q4&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;],
        datasets: [{ label: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Revenue (€k)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, data: [482, 591, 623, 705],
                     backgroundColor: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#ea580c&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; }]
      },
      options: { animation: false, responsive: false,
                 plugins: { legend: { display: true } } }
    });
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;device_scale_factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chart.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rendered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Already produce charts as SVG (ECharts SSR, D3, a matplotlib SVG export) or as a styled HTML table? Pass that markup directly — same call, no JavaScript needed. &lt;strong&gt;500 free renders/month, no card&lt;/strong&gt; to start: &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;get a key&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest trade-off:&lt;/strong&gt; this doesn't make Chrome disappear — it relocates it onto Rendex's infrastructure. You give up nothing technically hard; you pay a per-render fee, a network hop, and you send the chart markup to a vendor (a sync render is processed transiently and not stored). You gain: no &lt;code&gt;node-gyp&lt;/code&gt;, no &lt;code&gt;libcairo&lt;/code&gt;/&lt;code&gt;libpango&lt;/code&gt; in your image, no Chromium-in-CI, no font-package debugging, no kaleido-version breakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Chart.js + chartjs-node-canvas (Node, no browser)
&lt;/h2&gt;

&lt;p&gt;The canonical Node route renders Chart.js onto a &lt;a href="https://www.npmjs.com/package/chartjs-node-canvas" rel="noopener noreferrer"&gt;node-canvas&lt;/a&gt; surface — no browser, but you inherit node-canvas's native Cairo/Pango dependency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// npm i chartjs-node-canvas chart.js&lt;/span&gt;
&lt;span class="c1"&gt;// + system libs: apt-get install libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChartJSNodeCanvas&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chartjs-node-canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChartJSNodeCanvas&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renderToBuffer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;591&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;623&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;705&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chart.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; node-canvas's native build fails on fresh Node majors, Alpine, and ARM until prebuilt binaries catch up; fonts must be installed in the image or text renders as boxes. For new projects, the Skia-based &lt;code&gt;@napi-rs/canvas&lt;/code&gt; or &lt;code&gt;skia-canvas&lt;/code&gt; ship N-API prebuilts and avoid the compile (the old &lt;code&gt;canvas-prebuilt&lt;/code&gt; package is discontinued — don't use it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: matplotlib savefig (Python, the lightest deps)
&lt;/h2&gt;

&lt;p&gt;For Python, matplotlib with the &lt;code&gt;Agg&lt;/code&gt; backend is the only genuinely browser-free &lt;em&gt;and&lt;/em&gt; Cairo-free PNG path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install matplotlib
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;matplotlib&lt;/span&gt;
&lt;span class="n"&gt;matplotlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# MUST be set before importing pyplot (headless)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;

&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subplots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dpi&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;591&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;623&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;705&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#ea580c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revenue (€k)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;savefig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chart.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bbox_inches&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;# close every figure or you leak memory in a loop
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; you must force &lt;code&gt;Agg&lt;/code&gt; before importing pyplot or it tries a GUI and crashes headless; missing system fonts silently drop glyphs (especially CJK); and you &lt;strong&gt;leak memory&lt;/strong&gt; if you forget &lt;code&gt;plt.close()&lt;/code&gt; in a loop. Great for native matplotlib charts; it won't render an HTML/CSS report or a JS charting library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 4: pandas DataFrame → image (secretly headless Chrome)
&lt;/h2&gt;

&lt;p&gt;The popular &lt;a href="https://github.com/dexplo/dataframe_image" rel="noopener noreferrer"&gt;dataframe-image&lt;/a&gt; turns a styled DataFrame into a PNG — and its default backend &lt;em&gt;shells out to a local Chrome&lt;/em&gt;, which surprises people in containers and CI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install dataframe_image   (default backend needs a local Chrome)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataframe_image&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dfi&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Quarter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revenue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;591&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;623&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;705&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;span class="n"&gt;styled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background_gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revenue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cmap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Oranges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dfi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;styled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# uses local Chrome by default
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; the default &lt;code&gt;chrome&lt;/code&gt; backend is brittle in headless environments (it fails in Google Colab; you switch to the selenium/Firefox or matplotlib backends); the matplotlib fallback only simulates the default table style (no custom CSS, no captions). Note &lt;code&gt;df2img&lt;/code&gt; pins the deprecated kaleido v0 line — a maintenance risk for new code. If you're already styling the DataFrame as HTML (&lt;code&gt;df.style.to_html()&lt;/code&gt;), you've produced markup, not pixels — rendering it is the same Chrome problem, which is exactly what an HTML-render API solves (Method 1).&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 5: Plotly — mind the kaleido break
&lt;/h2&gt;

&lt;p&gt;Plotly's static export is the route most affected by 2025/26 changes. &lt;code&gt;fig.to_image()&lt;/code&gt; uses kaleido, but &lt;strong&gt;kaleido v1 no longer bundles Chromium&lt;/strong&gt; — if no compatible Chrome is on the box, export errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install --upgrade plotly kaleido
# kaleido v1 does NOT ship a browser — install Chrome or run: kaleido_get_chrome
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;plotly.graph_objects&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;go&lt;/span&gt;

&lt;span class="n"&gt;fig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;591&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;623&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;705&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chart.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# engine auto = kaleido (needs system Chrome)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; the old &lt;code&gt;engine="orca"&lt;/code&gt; path is &lt;strong&gt;deprecated and removed after September 2025&lt;/strong&gt; — migrate to kaleido. And kaleido &lt;strong&gt;v0&lt;/strong&gt; (bundled Chromium) is superseded by &lt;strong&gt;v1&lt;/strong&gt; (bring your own Chrome), so guides that rely on the bundled browser are stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 6: ECharts SSR — zero-dependency, but SVG not PNG
&lt;/h2&gt;

&lt;p&gt;Apache ECharts can render server-side with &lt;strong&gt;no native dependency at all&lt;/strong&gt; — the catch is it outputs an &lt;strong&gt;SVG string&lt;/strong&gt;, not a raster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// npm i echarts   (no Cairo, no Chrome — cleanest native-free route)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;echarts&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;echarts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;echarts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOption&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;xAxis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;yAxis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;591&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;623&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;705&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renderToSVGString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// SVG markup — rasterize for email/PDF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; if the consumer needs a PNG (email, Slack, a PDF embed), you still have to rasterize that SVG — via resvg, Sharp, or a browser. ECharts SSR gets you clean markup with no deps; turning it into pixels re-introduces a renderer. (That SVG is also a perfectly good input to POST to a rendering API — back to Method 1.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you use?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Heavy dependency&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rendex API (HTML/SVG → PNG)&lt;/td&gt;
&lt;td&gt;None on your box&lt;/td&gt;
&lt;td&gt;PNG / PDF&lt;/td&gt;
&lt;td&gt;Charts already expressible as HTML/SVG; no infra to own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chart.js + node-canvas&lt;/td&gt;
&lt;td&gt;Cairo/Pango (native)&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;Node apps that can carry the native build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;matplotlib (Agg)&lt;/td&gt;
&lt;td&gt;None (bundled C ext)&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;Native matplotlib charts; lightest Python deps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dataframe-image&lt;/td&gt;
&lt;td&gt;Headless Chrome&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;Styled pandas DataFrames (if Chrome is present)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plotly + kaleido v1&lt;/td&gt;
&lt;td&gt;System Chrome&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;Plotly figures (install Chrome separately)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECharts SSR&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;SVG (rasterize for PNG)&lt;/td&gt;
&lt;td&gt;Clean markup; you handle rasterization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest takeaway: there is no painless "draw some pixels" path that scales — you either own a native canvas stack or a headless browser. The in-process options (matplotlib, node-canvas) work well if you accept their build and font upkeep; ECharts SSR is clean but stops at SVG. If your charts and tables are already HTML/CSS/SVG — which, for most report and dashboard work, they are — the least infra is to POST that markup to &lt;a href="https://rendex.dev/use-cases/server-side-report-rendering" rel="noopener noreferrer"&gt;Rendex&lt;/a&gt; and let the browser live somewhere else. See the &lt;a href="https://rendex.dev/recipes/html-financial-report-to-png-python" rel="noopener noreferrer"&gt;Python report-rendering recipe&lt;/a&gt; for the full pattern, or &lt;a href="https://rendex.dev/blog/render-html-table-to-png-python" rel="noopener noreferrer"&gt;rendering an HTML table to PNG&lt;/a&gt; for the table case.&lt;/p&gt;

</description>
      <category>charts</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Best HTML-to-PDF Tools in 2026 (APIs + Libraries)</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Fri, 05 Jun 2026 17:53:14 +0000</pubDate>
      <link>https://dev.to/danelliot/best-html-to-pdf-tools-in-2026-apis-libraries-3gl5</link>
      <guid>https://dev.to/danelliot/best-html-to-pdf-tools-in-2026-apis-libraries-3gl5</guid>
      <description>&lt;h1&gt;
  
  
  Best HTML-to-PDF Tools in 2026 (APIs + Libraries)
&lt;/h1&gt;

&lt;p&gt;Every production web app eventually needs HTML-to-PDF. The options range from zero-setup APIs to self-hosted headless browsers, and picking the wrong one means either managing Chromium memory leaks in production or paying per-call rates that don't fit your volume.&lt;/p&gt;

&lt;p&gt;This roundup covers five tools against consistent criteria: CSS and JavaScript rendering quality, integration time, pricing model, and honest weaknesses. Five tools covered: Rendex, DocRaptor, PDFShift, wkhtmltopdf, and Playwright.&lt;/p&gt;

&lt;h2&gt;
  
  
  How These Tools Were Evaluated
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSS rendering&lt;/strong&gt;: flexbox, grid, CSS custom properties, &lt;code&gt;@media print&lt;/code&gt; support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript execution&lt;/strong&gt;: waits for dynamic content, React/Vue/Svelte app rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration time&lt;/strong&gt;: minutes from sign-up to first PDF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing&lt;/strong&gt;: free tier, per-call cost at 10k/month, overage handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure burden&lt;/strong&gt;: managed API vs. you own the servers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Rendex API
&lt;/h2&gt;

&lt;p&gt;Rendex runs Chromium on Cloudflare's global edge network. You POST a URL or raw HTML, get back a PDF. No browser to install, no memory to manage, no cold starts from a containerized Chrome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Replace rdx_live_YOUR_KEY with a key from https://rendex.dev/login&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_live_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com",
    "format": "pdf",
    "pdfFormat": "A4",
    "pdfMargin": "20mm",
    "pdfPrintBackground": true
  }'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; output.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTML-to-PDF works the same way. Swap the &lt;code&gt;url&lt;/code&gt; field for an &lt;code&gt;html&lt;/code&gt; field with your template string. Useful for invoice generation where the page never needs to exist at a public URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_live_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "html": "&amp;lt;h1&amp;gt;Invoice #1042&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Amount due: $250&amp;lt;/p&amp;gt;",
    "format": "pdf",
    "pdfFormat": "Letter",
    "pdfPrintBackground": true
  }'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; invoice.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; covers page size options (A4, Letter, Legal, Tabloid), landscape mode, margin control, and scale. Python and JavaScript SDKs are available for both sync and async workflows.&lt;/p&gt;

&lt;p&gt;For a deeper Python walkthrough, see &lt;a href="https://rendex.dev/blog/url-to-pdf-python" rel="noopener noreferrer"&gt;How to Generate PDFs from URLs in Python&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: Full Chromium rendering, no infrastructure to run, global edge delivery, async batch jobs for high-volume workloads, geo-targeting on Pro plans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: Monthly call caps per plan. If you need truly unlimited volume at predictable cost, the pricing model may not fit. The free tier gives you 500 calls/month to test before committing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free tier (500 calls/month). Paid plans available at &lt;a href="https://rendex.dev/pricing" rel="noopener noreferrer"&gt;rendex.dev/pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. DocRaptor
&lt;/h2&gt;

&lt;p&gt;DocRaptor uses the Prince XML rendering engine, which has been the reference implementation for the CSS Paged Media specification for over a decade. It handles complex print layouts, running headers and footers, page numbering, and advanced typographic features that Chromium-based tools handle inconsistently.&lt;/p&gt;

&lt;p&gt;The trade-off is cost. DocRaptor is priced for document-heavy enterprise workflows, not high-volume automated generation. It excels at reports that need precise pagination control and legal or publishing workflows where print CSS compliance matters more than price per call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: Best-in-class CSS Paged Media support, predictable print layout, long track record in publishing and legal tech.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: Expensive at scale. Not ideal for consumer-facing workflows with high call volumes. Fewer options for JavaScript-heavy SPAs compared to Chromium-based tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Legal documents, publishing pipelines, reports where pagination precision outweighs cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. PDFShift
&lt;/h2&gt;

&lt;p&gt;PDFShift is a hosted HTML-to-PDF API backed by a headless Chromium instance. The integration surface is minimal: one endpoint, one JSON body with &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;landscape&lt;/code&gt;, &lt;code&gt;margin&lt;/code&gt;, and a handful of other options.&lt;/p&gt;

&lt;p&gt;PDFShift offers an EU-hosted option for teams with data residency requirements, which DocRaptor and some other APIs do not. Pricing is credit-based, which can be more cost-effective than per-call pricing if your usage is bursty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: Simple API, EU data residency option, credit-based pricing suits variable workloads, good JavaScript support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: Less established ecosystem than Puppeteer or DocRaptor. Credit expiration on some plans can be a surprise. Fewer geographic edge locations than Cloudflare-backed options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams that need EU hosting, developers who want a simpler API surface than a full-featured rendering platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. wkhtmltopdf
&lt;/h2&gt;

&lt;p&gt;wkhtmltopdf is a command-line tool that converts HTML to PDF using a patched version of the QtWebKit engine. It requires no API call, works offline, and ships as a single binary in most Linux package managers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install: apt install wkhtmltopdf&lt;/span&gt;
&lt;span class="c"&gt;# Convert URL to PDF&lt;/span&gt;
wkhtmltopdf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--page-size&lt;/span&gt; A4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--margin-top&lt;/span&gt; 20mm &lt;span class="nt"&gt;--margin-bottom&lt;/span&gt; 20mm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--margin-left&lt;/span&gt; 20mm &lt;span class="nt"&gt;--margin-right&lt;/span&gt; 20mm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--print-media-type&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://example.com output.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is the engine. QtWebKit is effectively frozen at a 2012-era rendering baseline. Flexbox works partially, CSS Grid does not, and CSS custom properties are not supported. The project has been unmaintained since 2020. Modern web apps built with React, Vue, or Tailwind will render incorrectly or not at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: No API key required, no internet connection needed, fast for simple pages, available on every Linux distribution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: Outdated WebKit engine, no CSS Grid or custom property support, limited JavaScript execution, effectively unmaintained. Not usable for modern SPAs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Simple static HTML pages on isolated servers, legacy systems where adding an API call is not an option. Avoid for anything built in the last five years.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Playwright (Self-Hosted Chromium)
&lt;/h2&gt;

&lt;p&gt;Playwright gives you a full Chromium browser under programmatic control. PDF generation uses the same print engine as Chrome's built-in Save as PDF, which means you get accurate rendering for any modern CSS layout. You can authenticate, dismiss banners, wait for specific elements, and inject CSS before capture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// npm install playwright&lt;/span&gt;
&lt;span class="c1"&gt;// npx playwright install chromium&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;printBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost is infrastructure. Chromium uses ~300MB of RAM per browser instance. In a containerized environment with concurrent requests, you need to manage a browser pool, handle process crashes, and size your instances for peak load. On serverless platforms, Chromium requires special handling or a layer, and cold starts can add significant latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: Full modern CSS support, browser interaction before capture (logins, cookie banners, dynamic content), no per-call cost once running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: Heavy infrastructure cost. Memory leaks in long-running processes are a real problem. Not practical on serverless without workarounds. You own the uptime, scaling, and maintenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams that already run their own server infrastructure, workflows requiring browser interaction before PDF generation, high-volume scenarios where per-call API pricing becomes prohibitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;CSS Grid&lt;/th&gt;
&lt;th&gt;Hosted&lt;/th&gt;
&lt;th&gt;Free Tier&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rendex&lt;/td&gt;
&lt;td&gt;Chromium&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (CF edge)&lt;/td&gt;
&lt;td&gt;500/mo&lt;/td&gt;
&lt;td&gt;APIs, agents, batch jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DocRaptor&lt;/td&gt;
&lt;td&gt;Prince XML&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Trial only&lt;/td&gt;
&lt;td&gt;Legal, publishing, print CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDFShift&lt;/td&gt;
&lt;td&gt;Chromium&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (EU option)&lt;/td&gt;
&lt;td&gt;Trial credits&lt;/td&gt;
&lt;td&gt;EU data residency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wkhtmltopdf&lt;/td&gt;
&lt;td&gt;QtWebKit&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No (CLI)&lt;/td&gt;
&lt;td&gt;Free/open source&lt;/td&gt;
&lt;td&gt;Simple legacy pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;Chromium&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (self-host)&lt;/td&gt;
&lt;td&gt;Free/open source&lt;/td&gt;
&lt;td&gt;Self-hosted, browser interaction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which to Pick
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You need a PDF in under an hour with working CSS&lt;/strong&gt;: Rendex or PDFShift. Both are Chromium-backed APIs with free trials. Rendex includes batch jobs and MCP support for agent pipelines if that matters to your workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need complex print layouts&lt;/strong&gt;: DocRaptor. Prince XML's CSS Paged Media support is the best available. You'll pay for it, but the output quality for complex multi-page documents is not matched by Chromium-based tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have a data residency requirement (EU)&lt;/strong&gt;: PDFShift offers EU-hosted infrastructure. Verify their current data processing agreement before committing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You run your own servers and need browser interaction&lt;/strong&gt;: Playwright. Use it when the page requires authentication or dynamic interaction before PDF generation, and when you already have the infrastructure to run it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have simple static pages and no API budget&lt;/strong&gt;: wkhtmltopdf gets the job done on pages from 2015. If the page uses Tailwind, React, or any modern CSS, expect problems.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://rendex.dev/use-cases/url-to-pdf" rel="noopener noreferrer"&gt;URL-to-PDF use case page&lt;/a&gt; for a detailed breakdown of Rendex capabilities and integration patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;The fastest way to test any of the API options is the &lt;a href="https://rendex.dev/tools/url-to-pdf" rel="noopener noreferrer"&gt;free URL-to-PDF tool&lt;/a&gt;: paste a URL, get a PDF back, no sign-up required. It uses the same Rendex rendering pipeline as the API.&lt;/p&gt;

&lt;p&gt;Ready to automate? &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; (500 calls/month, no credit card) and convert your first URL to PDF in under five minutes.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>roundup</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Convert Markdown to PDF with an API (2026)</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Sat, 30 May 2026 17:02:48 +0000</pubDate>
      <link>https://dev.to/danelliot/how-to-convert-markdown-to-pdf-with-an-api-2026-5d46</link>
      <guid>https://dev.to/danelliot/how-to-convert-markdown-to-pdf-with-an-api-2026-5d46</guid>
      <description>&lt;h1&gt;
  
  
  How to Convert Markdown to PDF with an API (2026)
&lt;/h1&gt;

&lt;p&gt;Markdown is everywhere: README files, AI-generated docs, internal notes, changelogs. When you need to ship a PDF from that content, the standard options are headless Chrome (memory leaks, cold starts, ops overhead) or a three-step pipeline that converts Markdown to HTML, then HTML to PDF, across separate services. The markdown to pdf api in Rendex collapses that to one POST request.&lt;/p&gt;

&lt;p&gt;You pass a raw Markdown string. The API converts it to a styled HTML document server-side and renders it to a PDF or PNG. No Chromium to manage, no intermediate files, no conversion library to pin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An API key from &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;rendex.dev/login&lt;/a&gt; (free tier: 500 calls/month)&lt;/li&gt;
&lt;li&gt;curl, Python 3.8+, or Node.js 18+&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Make the API call
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;POST /v1/screenshot&lt;/code&gt; endpoint accepts a &lt;code&gt;markdown&lt;/code&gt; field alongside &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;html&lt;/code&gt;. The three are mutually exclusive: pick one per request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get your API key at rendex.dev/login&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_live_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "markdown": "# Q2 Report\n\n## Revenue\n\n- APAC: $18k\n- EMEA: $14k",
    "format": "pdf",
    "pdfFormat": "A4",
    "pdfPrintBackground": true
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; report.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;markdown&lt;/code&gt; field accepts up to 5 MB of Markdown text. The API converts it to HTML using a GitHub-flavored stylesheet before rendering, so standard Markdown syntax (headings, tables, fenced code blocks, inline code, blockquotes) renders as expected.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;format&lt;/code&gt; parameter controls the output type: &lt;code&gt;"pdf"&lt;/code&gt; returns a PDF document, &lt;code&gt;"png"&lt;/code&gt;, &lt;code&gt;"jpeg"&lt;/code&gt;, and &lt;code&gt;"webp"&lt;/code&gt; return images of the rendered page. &lt;code&gt;pdfFormat&lt;/code&gt; accepts &lt;code&gt;A4&lt;/code&gt;, &lt;code&gt;Letter&lt;/code&gt;, &lt;code&gt;Legal&lt;/code&gt;, &lt;code&gt;Tabloid&lt;/code&gt;, and &lt;code&gt;A3&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Capture the full document
&lt;/h2&gt;

&lt;p&gt;By default, the viewport height is 800px. For long Markdown documents, add &lt;code&gt;fullPage: true&lt;/code&gt; to capture the entire rendered height rather than just the above-the-fold view. This has no effect on PDF output, which always renders full-page, but matters for PNG and JPEG.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_live_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "markdown": "# Docs\n\n## Section 1\n\nContent here.\n\n## Section 2\n\nMore content.",
    "format": "png",
    "fullPage": true
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; docs.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Use the Python SDK
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://rendex.dev/docs/sdks" rel="noopener noreferrer"&gt;Rendex Python SDK&lt;/a&gt; ships a &lt;code&gt;render_markdown&lt;/code&gt; method that wraps the markdown to pdf api call and handles authentication, camelCase conversion, and response parsing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install rendex
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rendex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="c1"&gt;# Get your API key at rendex.dev/login
&lt;/span&gt;&lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rdx_live_YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;# Q2 Report

Generated: 2026-05-27

## Revenue

| Region  | Q1   | Q2   |
|---------|------|------|
| APAC    | $12k | $18k |
| EMEA    | $9k  | $14k |

## Notes

- Compare against **Q1 targets** before sharing.
- Export as PDF before the 3pm review.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdf_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q2-report.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PDF written: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bytes_size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;render_markdown&lt;/code&gt; is a convenience wrapper over &lt;code&gt;screenshot(markdown=...)&lt;/code&gt;. Both accept the same keyword arguments in snake_case, which the SDK converts to camelCase before sending the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Use the JavaScript SDK
&lt;/h2&gt;

&lt;p&gt;The TypeScript SDK on npm (&lt;code&gt;@copperline/rendex&lt;/code&gt;) exposes &lt;code&gt;renderMarkdown&lt;/code&gt;, a typed wrapper over &lt;code&gt;screenshot({ markdown })&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// npm install @copperline/rendex&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rendex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Get your API key at rendex.dev/login&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rdx_live_YOUR_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`# Release Notes

## v2.1.0

- Fixed auth timeout on long requests
- Added batch capture support (up to 500 URLs per job)
- Improved PDF margin defaults
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renderMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pdfFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pdfPrintBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;release-notes.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;release-notes.pdf written&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Override the default styles
&lt;/h2&gt;

&lt;p&gt;The default stylesheet is GitHub-flavored: 16px system font, &lt;code&gt;max-width: 768px&lt;/code&gt;, bordered H1 and H2, code blocks with a light gray background. To match your brand, pass a &lt;code&gt;css&lt;/code&gt; string (up to 50 KB). It is injected after the default stylesheet, so it wins the cascade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_live_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "markdown": "# Custom Report\n\nStyled with custom CSS.",
    "format": "pdf",
    "css": "body.rendex-markdown { max-width: 960px; font-size: 14px; } body.rendex-markdown h1 { color: #ea580c; border-bottom-color: #ea580c; }"
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; styled.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Target &lt;code&gt;body.rendex-markdown&lt;/code&gt; to override the container, or use any standard CSS selector.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can generate
&lt;/h2&gt;

&lt;p&gt;Markdown mode gives you clean GitHub styling out of the box. Layer a &lt;code&gt;css&lt;/code&gt; string on top, or switch to &lt;code&gt;html&lt;/code&gt; mode, and the same render pipeline turns plain input into production documents: invoices, reports, certificates, packing slips.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg3itq73pj8iip7enkyxs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg3itq73pj8iip7enkyxs.png" alt="Branded invoice PDF generated by the Rendex markdown to pdf api, with line items, tax, and total rendered from a template" width="800" height="1031"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pipeline handles long-form documents just as well. Author the body in Markdown and theme it with a &lt;code&gt;css&lt;/code&gt; string, or hand Rendex finished HTML.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5fg52q1ecppw90hhr02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5fg52q1ecppw90hhr02.png" alt="Styled product release notes rendered to PDF by Rendex, with a branded header, feature list, and a parameters table" width="800" height="924"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown mode vs HTML mode
&lt;/h2&gt;

&lt;p&gt;Use the &lt;code&gt;markdown&lt;/code&gt; parameter when your source is already Markdown and the default GitHub styling is close enough. Use the &lt;code&gt;html&lt;/code&gt; parameter when you need precise control over layout, custom fonts loaded from a CDN, or pixel-level design fidelity. The markdown pipeline adds a conversion step; HTML mode passes your markup directly to the renderer. Either way, the &lt;code&gt;css&lt;/code&gt; parameter can push styles on top.&lt;/p&gt;

&lt;p&gt;Both modes support Mustache templating via the &lt;code&gt;data&lt;/code&gt; parameter. Pass &lt;code&gt;{"data": {"name": "Alice"}}&lt;/code&gt; alongside your &lt;code&gt;markdown&lt;/code&gt; string and use &lt;code&gt;{{name}}&lt;/code&gt; in the Markdown body. The server fills the template before rendering. See the &lt;a href="https://rendex.dev/tools/markdown-to-image" rel="noopener noreferrer"&gt;Markdown-to-Image tool&lt;/a&gt; to try this in the browser without writing any code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;VALIDATION_ERROR: Provide exactly one of 'url', 'html', or 'markdown'&lt;/code&gt;&lt;/strong&gt;: the request body includes more than one source field. Remove &lt;code&gt;url&lt;/code&gt; or &lt;code&gt;html&lt;/code&gt; if you are using &lt;code&gt;markdown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;PAYLOAD_TOO_LARGE&lt;/code&gt;&lt;/strong&gt;: the Markdown string exceeds 5 MB, or the &lt;code&gt;data&lt;/code&gt; object exceeds 256 KB serialized. Split large documents or trim the data payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tables or fenced code blocks not rendering&lt;/strong&gt;: the default parser enables HTML mode so embedded HTML tags render as authored. Tables use standard GFM syntax (pipe-separated). Fenced blocks with a language identifier render without syntax highlighting; syntax coloring requires custom CSS or a client-side library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Font not loading in rendered output&lt;/strong&gt;: the renderer fetches web fonts from the URL in your &lt;code&gt;@import&lt;/code&gt; statement. If the font domain requires authentication or uses a private CDN, the renderer falls back to the system font. Use a public URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF missing page breaks between sections&lt;/strong&gt;: add a CSS page-break rule to the &lt;code&gt;css&lt;/code&gt; param: &lt;code&gt;h2 { page-break-before: always; }&lt;/code&gt;. The first H2 in the document will still start on page one; subsequent H2s will open new pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;The markdown to pdf api is one input mode of the same rendering pipeline. The same endpoint accepts a URL or raw HTML. For URL-to-PDF patterns in Python, see &lt;a href="https://rendex.dev/blog/url-to-pdf-python" rel="noopener noreferrer"&gt;URL to PDF with Python&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To try Markdown rendering in the browser without an API key, use the &lt;a href="https://rendex.dev/tools/markdown-to-image" rel="noopener noreferrer"&gt;Markdown-to-Image tool&lt;/a&gt;. Paste your content, pick PNG or PDF, and download the result.&lt;/p&gt;

&lt;p&gt;Start for free at &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;rendex.dev/login&lt;/a&gt;. The free tier covers 500 calls/month. No credit card required.&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>pdf</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Self-Hosted Puppeteer Costs More Than a Screenshot API</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Fri, 29 May 2026 18:31:37 +0000</pubDate>
      <link>https://dev.to/danelliot/why-self-hosted-puppeteer-costs-more-than-a-screenshot-api-3in1</link>
      <guid>https://dev.to/danelliot/why-self-hosted-puppeteer-costs-more-than-a-screenshot-api-3in1</guid>
      <description>&lt;h1&gt;
  
  
  Why Self-Hosted Puppeteer Costs More Than a Screenshot API
&lt;/h1&gt;

&lt;p&gt;Puppeteer works until it doesn't. Early in a project, running Chrome on a $20 VPS feels fine. Then traffic grows, concurrent requests pile up, and the tab that should take two seconds starts taking forty. Memory climbs. The process crashes. You add a cron job to restart it and call it done. Six months later, you have a team member whose job is keeping Chrome alive.&lt;/p&gt;

&lt;p&gt;This is the puppeteer vs screenshot api decision most teams make reactively instead of proactively. Below is a concrete cost breakdown of self-hosted Puppeteer, where the math tips toward an API, and when DIY still makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Self-Hosted Actually Costs
&lt;/h2&gt;

&lt;p&gt;Chrome is expensive to run. Each browser instance holds its own V8 heap, network stack, and renderer processes. A single headless Chrome tab uses roughly 300-400 MB of RAM at idle and spikes higher during active rendering. At five concurrent requests, that is 1.5-2 GB just for the browser, before your application server, database, or anything else running on the box.&lt;/p&gt;

&lt;p&gt;Here is a realistic monthly breakdown for a team running 5,000 screenshots per month at low-to-moderate concurrency (five parallel captures):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost Category&lt;/th&gt;
&lt;th&gt;Monthly Estimate&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EC2 t3.xlarge (4 vCPU, 16 GB)&lt;/td&gt;
&lt;td&gt;~$120&lt;/td&gt;
&lt;td&gt;Minimum for five concurrent tabs plus OS headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EBS storage + data transfer&lt;/td&gt;
&lt;td&gt;~$15&lt;/td&gt;
&lt;td&gt;OS image, Chrome binary, screenshot output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 storage for output files&lt;/td&gt;
&lt;td&gt;~$10&lt;/td&gt;
&lt;td&gt;At ~150 KB average per screenshot (WebP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring + alerting&lt;/td&gt;
&lt;td&gt;~$20&lt;/td&gt;
&lt;td&gt;Datadog, Better Uptime, or equivalent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering maintenance&lt;/td&gt;
&lt;td&gt;~$300-900&lt;/td&gt;
&lt;td&gt;2-6 hrs/month at $150/hr. Often more.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$465-1,065&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Before incident response time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The engineering maintenance figure is the one teams consistently undercount. Chromium updates break headless behavior. Puppeteer version bumps require regression testing. Memory leaks surface in production at 3 AM. Docker images grow. You add a queue. You add retries. Each fix is small; the total is a part-time job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs That Never Appear on the Cloud Bill
&lt;/h2&gt;

&lt;p&gt;Beyond line items, self-hosted Puppeteer carries invisible costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cold starts in serverless environments.&lt;/strong&gt; Chrome cannot run inside AWS Lambda at normal function sizes or in Cloudflare Workers on the standard tier. If you need serverless, you end up with a sidecar service or a library like &lt;code&gt;@sparticuz/chromium&lt;/code&gt;, which adds latency, maintenance, and its own version fragility.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Geo-targeting is a separate problem.&lt;/strong&gt; If a client needs screenshots from a specific country, self-hosted requires a proxy layer on top of Chrome. That is another vendor, another failure point, and another monthly line item.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Burst capacity requires manual work.&lt;/strong&gt; A batch of 500 URLs submitted at once will overwhelm a single instance. Scaling horizontally means a queue, a scheduler, and multiple Chrome processes coordinated across machines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Storage and delivery are your problem.&lt;/strong&gt; Screenshots need to land somewhere accessible. S3 with signed URLs, CDN configuration, expiry policies, and access control are all table-stakes work that an API handles for you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security patching.&lt;/strong&gt; Chrome ships security updates frequently. Each one requires you to pull a new image, test, and redeploy. On a managed API, the vendor handles this.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the API Approach Looks Like
&lt;/h2&gt;

&lt;p&gt;The puppeteer vs screenshot api calculation changes when you count the full cost of self-hosted. Here is the same task using a dedicated rendering API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One request, no infrastructure to manage&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_your_key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com",
    "format": "webp",
    "fullPage": true
  }'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; screenshot.webp
&lt;span class="c"&gt;# Get your key at https://rendex.dev/login&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or using the JavaScript SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// npm install @copperline/rendex&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rendex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// Get your key at https://rendex.dev/login&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rdx_your_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// image is a Buffer — write to disk or upload directly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 5,000 captures per month, cost lands on the &lt;a href="https://rendex.dev/pricing" rel="noopener noreferrer"&gt;Starter plan&lt;/a&gt;. Compare that to the $465-1,065 range above. The 500 free calls per month on the free tier let you validate the integration before spending anything.&lt;/p&gt;

&lt;p&gt;For a feature-by-feature comparison with pricing, the &lt;a href="https://rendex.dev/compare/rendex-vs-screenshotone" rel="noopener noreferrer"&gt;Rendex vs ScreenshotOne breakdown&lt;/a&gt; covers both products honestly, including where ScreenshotOne has the edge. And if you need to process URLs in bulk, the &lt;a href="https://rendex.dev/use-cases/async-batch-rendering" rel="noopener noreferrer"&gt;async batch rendering overview&lt;/a&gt; shows how queue-based workflows operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Self-Hosted Puppeteer Wins
&lt;/h2&gt;

&lt;p&gt;Self-hosted is the right call in specific situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You already run Playwright or Puppeteer for testing.&lt;/strong&gt; If your CI pipeline runs browser tests on a dedicated fleet, adding screenshot generation on the same infrastructure costs almost nothing marginal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You need multi-step browser interaction before capture.&lt;/strong&gt; Login flows, cookie banners that require clicks, SPAs that navigate through several states before reaching the target view. APIs can inject cookies and headers, but a full click-through auth flow still requires self-hosted control.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Volume is very high and you are cost-sensitive.&lt;/strong&gt; Above roughly 100,000 screenshots per month, a dedicated server may cost less than per-call API pricing. Run the numbers at your actual volume.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern: teams with fewer than 50,000 captures per month and no existing browser automation fleet almost always come out ahead on total cost with an API. Teams above that threshold with infrastructure already in place often find self-hosted cheaper on compute, though the operational overhead rarely disappears entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the API Side of the Comparison
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://rendex.dev/docs/quickstart" rel="noopener noreferrer"&gt;quickstart guide&lt;/a&gt; gets you from zero to your first screenshot in under five minutes. The free tier includes 500 calls per month with no credit card required. If you want to test before writing any code, the &lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;browser-based screenshot tool&lt;/a&gt; runs a live capture against any public URL.&lt;/p&gt;

&lt;p&gt;If you're still deciding, start with the free tier on a single service or pipeline and measure. The difference in operational load is usually apparent within a week. &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; and see where the time actually goes.&lt;/p&gt;

</description>
      <category>comparison</category>
      <category>performance</category>
      <category>cost</category>
    </item>
    <item>
      <title>How to Convert HTML to an Image with an API (2026)</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Fri, 10 Apr 2026 23:09:52 +0000</pubDate>
      <link>https://dev.to/danelliot/how-to-convert-html-to-an-image-with-an-api-2026-np3</link>
      <guid>https://dev.to/danelliot/how-to-convert-html-to-an-image-with-an-api-2026-np3</guid>
      <description>&lt;p&gt;Converting HTML to an image is one of the most useful rendering operations in modern development. Social cards, invoices, OG images, email previews, and certificates — all start as HTML templates and need to become PNGs or JPEGs.&lt;/p&gt;

&lt;p&gt;This guide covers three approaches: a dedicated rendering API (fastest to ship), Puppeteer (the DIY standard), and a serverless function with Satori (for React-specific templates). Each includes working code you can copy and run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Rendex API (Fastest)
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://rendex.dev/use-cases/html-to-image" rel="noopener noreferrer"&gt;Rendex HTML-to-image API&lt;/a&gt; handles browser rendering on Cloudflare's edge network. Pass HTML as a string, get back a PNG, JPEG, WebP, or PDF. No Chromium install, no infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install rendex
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rendex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;div style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width:1200px;height:630px;display:flex;align-items:center;
  justify-content:center;background:linear-gradient(135deg,#667eea,#764ba2);
  font-family:system-ui;color:white;font-size:48px;font-weight:bold;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
  Hello from Rendex
&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;card.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generated &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bytes_size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The equivalent JavaScript SDK call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Rendex&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
&amp;lt;div style="width:1200px;height:630px;display:flex;align-items:center;
  justify-content:center;background:linear-gradient(135deg,#667eea,#764ba2);
  font-family:system-ui;color:white;font-size:48px;font-weight:bold;"&amp;gt;
  Hello from Rendex
&amp;lt;/div&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with raw HTTP — no SDK needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "html": "&amp;lt;div style=\"width:1200px;height:630px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#667eea,#764ba2);font-family:system-ui;color:white;font-size:48px;font-weight:bold;\"&amp;gt;Hello from Rendex&amp;lt;/div&amp;gt;",
    "format": "png",
    "width": 1200,
    "height": 630
  }'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; card.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; Production workloads, batch generation, and any case where you don't want to manage browser infrastructure. 500 free calls/month to start. &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get an API key&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Puppeteer (Self-Hosted)
&lt;/h2&gt;

&lt;p&gt;Puppeteer gives you a headless Chromium instance you control. You can render any HTML by loading it as a data URL or setting page content directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
&amp;lt;div style="width:1200px;height:630px;display:flex;align-items:center;
  justify-content:center;background:linear-gradient(135deg,#667eea,#764ba2);
  font-family:system-ui;color:white;font-size:48px;font-weight:bold;"&amp;gt;
  Hello from Puppeteer
&amp;lt;/div&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setViewport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkidle0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;screenshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; You need full browser control, want to run on your own infrastructure, or have specialized rendering requirements (e.g., browser extensions, network interception).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Requires Chromium (~400 MB), significant memory per instance, cold starts in serverless, and you manage the infrastructure. At scale, browser management becomes a full-time ops concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Satori + Resvg (React to SVG to PNG)
&lt;/h2&gt;

&lt;p&gt;Vercel's Satori converts React JSX to SVG, and resvg converts SVG to PNG. This approach avoids a browser entirely but only supports a subset of CSS (Flexbox only, no Grid).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;satori&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;satori&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Resvg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@resvg/resvg-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./Inter-Bold.ttf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;satori&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linear-gradient(135deg, #667eea, #764ba2)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Inter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;white&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from Satori&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Inter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;normal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resvg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Resvg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resvg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;asPng&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; You're already in a React/Next.js ecosystem, need zero external dependencies, and your templates use only Flexbox layouts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; No CSS Grid, limited CSS property support, no JavaScript execution, no web font loading (fonts must be bundled), and complex layouts may not render correctly. Great for simple cards, but not a general-purpose renderer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Method Should You Choose?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;CSS Support&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rendex API&lt;/td&gt;
&lt;td&gt;30 seconds&lt;/td&gt;
&lt;td&gt;Full (Chromium)&lt;/td&gt;
&lt;td&gt;Production, batch, AI agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Puppeteer&lt;/td&gt;
&lt;td&gt;10 minutes&lt;/td&gt;
&lt;td&gt;Full (Chromium)&lt;/td&gt;
&lt;td&gt;Self-hosted, full browser control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Satori + Resvg&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;td&gt;Flexbox only&lt;/td&gt;
&lt;td&gt;Simple cards, React ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most production use cases, an API is the right choice. You get full CSS support, zero infrastructure, and the ability to batch hundreds of renders in a single request.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://rendex.dev/use-cases/html-to-image" rel="noopener noreferrer"&gt;HTML to Image use case page&lt;/a&gt; for more examples, or &lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;try the free tool&lt;/a&gt; to render HTML without writing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Use Cases for HTML-to-Image
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rendex.dev/use-cases/og-image-generation" rel="noopener noreferrer"&gt;OG images&lt;/a&gt;&lt;/strong&gt; — Generate unique social preview images for every blog post, product page, or user profile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rendex.dev/use-cases/social-cards" rel="noopener noreferrer"&gt;Social cards&lt;/a&gt;&lt;/strong&gt; — Branded Twitter/LinkedIn cards from HTML templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rendex.dev/use-cases/invoice-rendering" rel="noopener noreferrer"&gt;Invoices&lt;/a&gt;&lt;/strong&gt; — HTML-to-PDF invoice generation with dynamic data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificates&lt;/strong&gt; — Course completion certificates with custom names and dates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email previews&lt;/strong&gt; — Render HTML emails as images for testing and review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ready to start? &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; (500 calls/month, no credit card) or &lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;read the API reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Want to see how Rendex compares to other rendering APIs? &lt;a href="https://rendex.dev/compare/best-screenshot-apis-2026" rel="noopener noreferrer"&gt;See the 2026 comparison&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Capture Website Screenshots with Python in 2026</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Thu, 09 Apr 2026 01:11:02 +0000</pubDate>
      <link>https://dev.to/danelliot/how-to-capture-website-screenshots-with-python-in-2026-4l5</link>
      <guid>https://dev.to/danelliot/how-to-capture-website-screenshots-with-python-in-2026-4l5</guid>
      <description>&lt;p&gt;Capturing website screenshots programmatically is one of the most common automation tasks for developers. Whether you're building a link previewer, monitoring a competitor's page, or generating OG images, you need a reliable way to render a webpage and save it as an image.&lt;/p&gt;

&lt;p&gt;This guide covers four approaches — from the fastest (an API call) to the most hands-on (a headless browser you manage yourself). Each includes working Python code you can copy and run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Rendex API (3 Lines of Code)
&lt;/h2&gt;

&lt;p&gt;The fastest path. The &lt;a href="https://rendex.dev/docs/quickstart" rel="noopener noreferrer"&gt;Rendex Python SDK&lt;/a&gt; handles browser rendering on Cloudflare's edge network — no Chromium install, no memory management, no cold starts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install rendex
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rendex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenshot.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bytes_size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_time_ms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Async support is built in — ideal for batch jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncRendex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;capture_many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;AsyncRendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.webp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;capture_many&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://news.ycombinator.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://stripe.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advanced options include dark mode, ad blocking, element selectors, custom viewports, and WebP output. See the &lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;full API reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; Production workloads, AI agent pipelines, batch processing. You get 500 free calls/month to start. &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get an API key&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Playwright
&lt;/h2&gt;

&lt;p&gt;Microsoft's Playwright is the current gold standard for headless browser automation. It supports Chromium, Firefox, and WebKit, with an excellent Python API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install playwright
# python -m playwright install chromium
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;networkidle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenshot.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright gives you full control — you can click buttons, fill forms, wait for specific elements, and intercept network requests before capturing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; You need browser interaction before capture (login flows, cookie banners, SPAs that require clicks). You manage the infrastructure yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Requires a Chromium install (~400 MB), consumes significant memory per browser instance, and cold starts can be slow in serverless environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Selenium
&lt;/h2&gt;

&lt;p&gt;Selenium has been the go-to browser automation tool since 2004. If your team already uses it for testing, adding screenshot capture is straightforward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install selenium webdriver-manager
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.service&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;webdriver_manager.chrome&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChromeDriverManager&lt;/span&gt;

&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ChromeOptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--headless=new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--window-size=1280,720&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--disable-gpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChromeDriverManager&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenshot.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; Existing Selenium test suites where you want to add screenshot capture without introducing a new tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Slower than Playwright, less reliable waiting mechanisms, and the WebDriver protocol adds overhead. Selenium 4+ improved significantly, but Playwright is generally preferred for new projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 4: Pyppeteer
&lt;/h2&gt;

&lt;p&gt;Pyppeteer is a Python port of Puppeteer (Node.js). It uses the Chrome DevTools Protocol directly, which gives fine-grained control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install pyppeteer
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyppeteer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;launch&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setViewport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;waitUntil&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;networkidle0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;screenshot.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fullPage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; You need DevTools Protocol access for advanced features like performance tracing or network interception, and prefer Python over Node.js.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; The project is less actively maintained than Playwright. Consider Playwright for new projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Method Should You Choose?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Setup Time&lt;/th&gt;
&lt;th&gt;Infra Needed&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rendex API&lt;/td&gt;
&lt;td&gt;30 seconds&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Production, AI agents, batch jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;td&gt;Chromium install&lt;/td&gt;
&lt;td&gt;Browser interaction before capture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selenium&lt;/td&gt;
&lt;td&gt;10 minutes&lt;/td&gt;
&lt;td&gt;ChromeDriver&lt;/td&gt;
&lt;td&gt;Existing Selenium test suites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pyppeteer&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;td&gt;Chromium install&lt;/td&gt;
&lt;td&gt;DevTools Protocol access&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most production use cases — especially if you're building an AI agent pipeline or need screenshots at scale — an API is the right choice. You avoid managing browsers, handling memory, and dealing with cold starts.&lt;/p&gt;

&lt;p&gt;Try the &lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;free screenshot tool&lt;/a&gt; to see Rendex in action without writing any code, or &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;grab an API key&lt;/a&gt; and start with 500 free calls/month.&lt;/p&gt;

&lt;p&gt;Want to see how Rendex compares to other APIs? &lt;a href="https://rendex.dev/compare/best-screenshot-apis-2026" rel="noopener noreferrer"&gt;See the 2026 comparison&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
