How to Set UTF-8 Encoding for wkhtmltopdf Footer HTML
Fix garbled accents, Chinese characters, and substituted values in wkhtmltopdf footer HTML with UTF-8 headers, decoding, and fonts.
Use three protections together: save the footer document as UTF-8, declare its charset in HTML, and run wkhtmltopdf with --encoding UTF-8. If your footer reads values from the query string, URL-encode them before transport and decode them once with decodeURIComponent(). When characters still disappear, check the actual bytes, HTTP response headers, installed fonts, locale, and wkhtmltopdf build separately.
1. The reliable configuration
A footer is loaded as its own HTML resource. The input document’s encoding setting does not replace an explicit charset declaration in that resource.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PDF footer</title>
<style>
body { margin: 0; font: 9pt Arial, sans-serif; }
</style>
</head>
<body>
<span class="mytitle">Résumé · 中文 · Ελληνικά</span>
</body>
</html>
Save footer.html as UTF-8 without converting it through a legacy code page. Then render it with:
wkhtmltopdf \
--encoding UTF-8 \
--footer-html footer.html \
input.html output.pdf
The compatibility declaration below is also valid and is recommended by django-wkhtmltopdf documentation:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
2. A complete footer with page variables and UTF-8 query values
wkhtmltopdf exposes substitution values such as [page], [topage], [webpage], [section], and [title]. For custom values, a common pattern is to append URL query parameters and read them in the footer JavaScript.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { margin: 0 12mm; color: #333; font: 8pt sans-serif; }
.row { display: flex; justify-content: space-between; }
</style>
</head>
<body>
<div class="row">
<span class="mytitle"></span>
<span>Page [page] of [topage]</span>
</div>
<script>
function subst() {
const params = new URLSearchParams(window.location.search);
document.querySelector('.mytitle').textContent = params.get('mytitle') || '';
}
window.onload = subst;
</script>
</body>
</html>
Encode the value before putting it in the URL. Decode it exactly once in the footer. Do not use the deprecated unescape() function.
title='Rapport — résumé 中文'
encoded=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' "$title")
wkhtmltopdf \
--encoding UTF-8 \
--footer-html "footer.html?mytitle=$encoded" \
input.html output.pdf
With a server-side footer, the same rule applies: percent-encode UTF-8 bytes for transport, then call decodeURIComponent() once. Reading with URLSearchParams is preferable because it handles query parsing without manually splitting on ampersands.
3. Local and remote footer sources
Local file
A local footer must be readable by the account running wkhtmltopdf. Use an absolute path when a service changes its working directory:
wkhtmltopdf --encoding UTF-8 \
--footer-html /srv/pdf/footer.html \
/srv/pdf/input.html /srv/pdf/output.pdf
Remote URL
If --footer-html points to an HTTP(S) URL, the response should identify a UTF-8-compatible content type, for example:
Content-Type: text/html; charset=utf-8
Check the response itself, redirects, authentication, and whether the rendering service can reach the host. A correct local file does not compensate for a remote response that is mislabeled or inaccessible.
4. Why --encoding UTF-8 sometimes appears not to work
| Layer | What it controls | Typical failure |
|---|---|---|
| File bytes | The bytes stored in HTML | Text was saved as Windows-1252 or another legacy encoding |
| HTML declaration | How the footer interprets its bytes | No charset or a conflicting declaration |
--encoding |
Default encoding for input | Expected to repair already-corrupted bytes |
| URL transport | Query-string values | Percent-encoded data decoded with unescape() or twice |
| HTTP headers | Encoding of remote footer responses | Missing or incorrect charset |
| Fonts | Glyphs available to the renderer | Boxes or missing Chinese, Arabic, emoji, or accented glyphs |
| Runtime | Build, OS, locale, and service account | Works interactively but fails in production |
--encoding UTF-8 is a default input setting. It cannot repair bytes that were already mis-encoded, bad URL decoding, or a missing glyph font.
5. Troubleshooting checklist
Accented characters are garbled
- Inspect the file bytes with your editor or a byte-level tool and resave as UTF-8.
- Add
<meta charset="utf-8">to the footer itself. - Keep
--encoding UTF-8on the command line. - Confirm that the source data was UTF-8 before it reached your template.
Hard-coded text works, but a substituted value is corrupted
Encode the value in the URL and replace unescape() with decodeURIComponent(). Ensure the value is decoded exactly once.
Chinese characters or another script are blank or shown as boxes
This is often a font problem rather than a charset problem. Install a font covering the required script and make sure the same font installation is visible to the production service account. Issue reports include cases where missing Chinese fonts explained the apparent encoding failure; see the wkhtmltopdf issue record.
footer-text drops characters
Use --footer-html for rich or non-ASCII content. Some environments have reported dropped characters through text-only footer paths, while an HTML footer with an explicit charset worked.
The footer is empty
- Check the path, permissions, and current working directory.
- For a URL, test DNS, TLS, redirects, authentication, and response status from the rendering host.
- Confirm the footer HTML is complete and has a body element.
- Reduce the case to one input page and one footer.
It works on a laptop but not in production
Capture the exact wkhtmltopdf version, operating system, locale, installed fonts, service account, and command line. Reproduce with a minimal HTML/CSS/JavaScript file containing one non-ASCII string.
6. A minimal diagnostic test
Use this test to separate charset, substitution, and font issues:
<!doctype html>
<meta charset="utf-8">
<div>ASCII | Café | € | 中文 | العربية</div>
<script>
const p = new URLSearchParams(location.search);
document.body.insertAdjacentHTML('beforeend', '<div>' +
(p.get('value') || '') + '</div>');
</script>
wkhtmltopdf --encoding UTF-8 \
--footer-html "diagnostic-footer.html?value=Résumé%20中文" \
input.html diagnostic.pdf
If hard-coded characters fail, inspect file bytes, the charset declaration, and fonts. If only the query value fails, inspect URL encoding and decoding. If the local footer passes but the remote one fails, inspect HTTP headers and transport.
7. API and library settings
The libwkhtmltox API accepts settings as UTF-8 encoded strings. Set the equivalent of web.defaultEncoding to UTF-8 and provide the footer HTML URL through the header/footer HTML setting. The same file-byte, URL-decoding, HTTP-header, and font checks still apply; an API wrapper cannot fix corrupted input.
8. Performance and reliability considerations
- Keep footer HTML small and self-contained so every page does not wait on extra network resources.
- Prefer local assets or a reachable, stable host for remote footers.
- Use a deterministic font installation across workers; different fonts can change glyph fallback and PDF layout.
- Pin and record the wkhtmltopdf build when diagnosing differences between environments.
- Test a one-page document first, then a multi-page document with long titles and non-ASCII values.
- Do not assume a successful process exit means every glyph rendered; inspect the resulting PDF.
9. Or skip the browser setup
If your goal is a clean screenshot or PDF of a web page rather than maintaining a wkhtmltopdf runtime, ScreenshotNeo provides a single HTTP request. Its API accepts a URL and returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for all options.
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}`);
Before capture, cookie and consent banners, newsletter popups, and chat widgets are removed. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
10. FAQ
Do I need both a meta charset and --encoding UTF-8?
Use both. The meta declaration makes the footer’s interpretation explicit, while the command-line option sets the default for the input document and integrations.
Should I decode footer parameters with decodeURIComponent()?
Yes, when the value was percent-encoded for the query string. Decode once and never use deprecated unescape().
Can UTF-8 fix missing emoji?
Only if a font available to wkhtmltopdf contains those glyphs. Encoding and font coverage are separate requirements.
Is a remote footer equivalent to a local footer?
Only after transport is verified. The remote response must be reachable and serve HTML with a UTF-8-compatible content type.
What should I include in a bug report?
Include the wkhtmltopdf version, OS and locale, exact command, service account, installed fonts, footer file or URL, response headers, and a minimal one-page reproduction.


