ScreenshotNeo

BlogHTML to image & PDF

HTML to PDF Go Library: Choosing and Implementing the Right Renderer

Compare pure-Go, wkhtmltopdf, and Chromium approaches, then convert HTML to PDF in Go with deployment, security, and troubleshooting guidance.

By the ScreenshotNeo team1 October 20267 min read

Short answer: there is no single best HTML-to-PDF Go library. Choose a pure-Go renderer when static HTML, simple print CSS, and a small deployment are more important than browser fidelity; choose a wkhtmltopdf binding when you already operate wkhtmltox; choose Chromium through Go when your documents depend on modern CSS, JavaScript, web fonts, or browser-like layout. Render representative invoices and reports before committing.

This guide explains the trade-offs, includes runnable Go code, and covers deployment, security, reliability, performance, and cost.

1. Decide which architecture fits your HTML

Approach Fidelity Deployment Use it when
Pure-Go renderer The gowkhtmltopdf documentation says it does not provide full CSS, JavaScript, or Chrome parity. Static, no-cgo binaries are documented; its current requirements list Go 1.26+. Templates are mostly static and a self-contained binary matters.
wkhtmltox Go binding Qt WebKit based; do not assume modern browser feature parity. Requires the native wkhtmltox library. Conversion calls must run on the main thread. You need the wkhtmltopdf settings surface and already manage its native dependency.
Chromium via Go Uses a real browser engine and is generally the strongest fit for modern CSS and JavaScript. A browser binary is an operational dependency; validate startup, fonts, sandboxing, and resource use. Pages need JavaScript, web fonts, flexbox/grid behavior, or browser-compatible output.

The wkhtmltopdf project describes its tools as LGPLv3 command-line programs that render HTML with Qt WebKit. Review the license and packaging requirements for your distribution at the project website.

2. A practical Go implementation with wkhtmltopdf

The following program sends HTML on standard input to the installed wkhtmltopdf executable. It is a complete Go wrapper that works well for controlled, server-side templates.

package main

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"os/exec"
	"time"
)

func main() {
	html := []byte(`<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>body{font-family:Arial,sans-serif;margin:32px} h1{color:#222}</style>
</head>
<body><h1>Invoice</h1><p>Rendered by Go and wkhtmltopdf.</p></body>
</html>`)

	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, "wkhtmltopdf", "--quiet", "-", "invoice.pdf")
	cmd.Stdin = bytes.NewReader(html)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			panic("PDF conversion timed out")
		}
		panic(fmt.Errorf("wkhtmltopdf failed: %w", err))
	}
}

Install wkhtmltopdf using the package method supported by your operating system, then run:

go mod init example.com/htmlpdf
go run .

For production, pass an explicit executable path if your installation is not on PATH, write output to a temporary file, check the exit status, and atomically rename the completed PDF into place.

Important wkhtmltopdf constraints

  • It uses Qt WebKit, so modern browser APIs and CSS may render differently from Chrome.
  • When using the adrg/go-wkhtmltopdf binding, install wkhtmltox and run conversion on the main thread as required by its documentation.
  • The binding README raises upstream maintenance concerns. Pin the native version and test upgrades.
  • Never pass arbitrary user URLs directly to a server-side renderer without SSRF controls.

3. Pure-Go rendering with gowkhtmltopdf

gowkhtmltopdf documents a Go library API and static, no-cgo builds. Its capability table explicitly says it is not full CSS, JavaScript, or Chrome parity, so it is best evaluated against your real templates rather than selected by package name alone. The getting-started documentation identifies release 0.2.6; LibraryVersion 0.12.7-dev is a wkhtmltopdf settings-surface compatibility identifier, not the project release.

Before adopting it, verify the current package API, Go toolchain requirement, print CSS support, font handling, page-break behavior, and local-file policy in the version you will ship. Keep a small corpus of expected PDFs and compare output after upgrades.

4. Chromium from Go

A Go PDF adapter can expose wkhtmltopdf and Chromium via chromedp as interchangeable engines. A Chromium path is appropriate when JavaScript must execute or when CSS must match a current browser. Your implementation must still define browser startup, navigation timeouts, network-idle behavior, font installation, page sizing, and process cleanup.

For either engine, wait for a deterministic application signal such as window.renderComplete = true instead of relying only on a fixed sleep. Include print CSS:

@media print {
  @page { size: A4; margin: 14mm; }
  .page-break { break-before: page; }
  thead { display: table-header-group; }
  tr, img { break-inside: avoid; }
}

5. Make HTML deterministic before conversion

  1. Inline or version your CSS and JavaScript so a deployment cannot change the document halfway through rendering.
  2. Install every required font in the image or host. Missing fonts change line wrapping and pagination.
  3. Use absolute URLs or an allowlisted asset host for images, stylesheets, and fonts.
  4. Wait for data requests and images to finish. For JavaScript applications, expose a completion marker.
  5. Set page size, margins, orientation, headers, and footers explicitly.
  6. Use stable locale, timezone, and number/date formatting in the template.

6. Security: treat HTML conversion as a network boundary

HTML-to-PDF engines fetch resources and execute content. The gowkhtmltopdf getting-started guidance says local files are blocked by default in its described target and warns that arbitrary user URLs can create SSRF exposure. Apply the same caution to every engine.

  • Allowlist destination hosts and schemes; reject private, loopback, link-local, and metadata addresses.
  • Run conversion in a restricted container or worker with least-privilege filesystem and network access.
  • Limit document size, page count, CPU time, memory, subprocess count, and downloaded bytes.
  • Do not inject untrusted HTML into privileged templates without sanitizing it.
  • Keep temporary files private and delete them after successful upload.

7. Reliability and performance checklist

  • Reuse workers: browser startup is expensive; keep a bounded pool when using Chromium.
  • Bound concurrency: PDF rendering is CPU and memory intensive. Measure peak RSS under realistic page counts.
  • Cache immutable assets: fonts and static CSS should not be downloaded for every document.
  • Use idempotency: derive a document key from template version and input data so retries do not create duplicates.
  • Capture diagnostics: retain renderer version, elapsed time, page count, exit status, and stderr.
  • Retry selectively: retry transient network failures; do not endlessly retry malformed HTML or a blocked destination.

No comparable benchmark establishes a universal speed winner among these approaches. Benchmark your own templates with cold and warm starts, JavaScript-heavy pages, large tables, remote assets, and concurrent jobs.

8. Troubleshooting

Symptom Likely cause Fix
Blank PDF JavaScript had not finished, or the URL was blocked. Wait for a completion marker, inspect stderr, and render from an allowlisted reachable origin.
Missing images or fonts Relative URLs, blocked network access, or missing host fonts. Use absolute URLs, permit required hosts, and install/package the fonts.
Layout differs from Chrome wkhtmltopdf uses Qt WebKit and lacks full modern browser parity. Switch to Chromium or simplify CSS and validate print styles.
Conversion hangs A resource never responds or a renderer process leaked. Set context deadlines, network timeouts, resource limits, and guaranteed process cleanup.
Binding crashes or behaves inconsistently wkhtmltox version mismatch or conversion off the main thread. Pin the native library and follow the binding’s main-thread requirement.
SSRF finding User-controlled URL reached an internal service. Use host allowlists, DNS/IP validation, egress filtering, and isolated workers.
Unexpected page breaks Unstable fonts, table rows, or missing print CSS. Declare fonts and @page rules; use break-inside: avoid and test long content.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API that can return PNG, JPEG, WebP, or PDF from one GET request. It accepts the cookie or consent banner like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets you turn each cleanup step off. Only clean shots are billed: bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, with X-Page-Verdict and X-Billed headers explaining the result.

For a URL-to-PDF or capture workflow, start with the API documentation at screenshotneo.com/docs and use:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also provides full-page capture with lazy images loaded, CSS-selector element capture, custom CSS and JavaScript, wait conditions, request blocking, headers and cookies, device and viewport controls, PDF paper and margin options, caching, signed links, asynchronous jobs, webhooks, bulk capture, usage reporting, and an MCP server with take_screenshot, get_page_info, and capture_pdf for AI agents. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

10. FAQ

Is a pure-Go library always the easiest deployment?

It can remove native browser dependencies, but confirm its CSS, JavaScript, font, and page-break behavior against your documents first.

Should invoices use wkhtmltopdf or Chromium?

Use wkhtmltopdf only when its WebKit behavior matches your templates and native installation is acceptable. Choose Chromium when modern browser rendering or JavaScript is required.

Can I safely convert a customer-supplied URL?

Only with SSRF defenses, strict allowlists, resource limits, and an isolated worker. Treat remote HTML as untrusted input.

How do I select a library without a benchmark?

Create a representative fixture set, render it with each candidate, inspect visual and textual output, then measure cold starts, warm throughput, memory, and failure recovery in your deployment environment.