April 21, 2026

Migrating from wkhtmltopdf: @page CSS, Fonts & API Guide

wkhtmltopdfmigrationPDF generationCSStutorial

You've been using wkhtmltopdf to generate PDFs. Now @page { size: A4; margin: 0; } isn't working as expected, Japanese text shows as tofu boxes, or your CSS Grid layout is completely broken.

These aren't bugs in your code — they're limitations of wkhtmltopdf's aging WebKit engine. And the situation isn't going to improve: wkhtmltopdf's development effectively stopped in 2020.

This guide covers what's different between wkhtmltopdf and modern Chromium-based PDF engines, and shows you exactly how to migrate to FUNBREW PDF with working code in curl, Python, and Node.js.

wkhtmltopdf's Current Status

End of Active Maintenance

wkhtmltopdf's last stable release was 0.12.6, released in 2020. The maintainers have officially indicated that active development has ended. Open issues include:

  • No security patches for known vulnerabilities
  • No Qt 5 migration completed
  • No support for modern CSS features added
  • No PDF/A compliance added

Why This Matters for Production

Risk Impact
No security patches SSRF and XSS attack vectors remain open
Growing CSS gaps Every UI update risks breaking PDF output
OS-level font dependency Server migrations can silently break Japanese rendering
No modern CSS CSS Grid, Flexbox, custom properties all unavailable

CSS Compatibility: wkhtmltopdf vs Chromium

Here's what actually breaks when you use modern CSS with wkhtmltopdf.

@page Rule Behavior

The most commonly reported issue:

/* Chromium: works exactly as specified */
/* wkhtmltopdf: margin may be ignored — CLI options take precedence */
@page {
  size: A4;
  margin: 0;
}

/* Chromium only: first-page overrides */
@page :first {
  margin-top: 60mm;
}

/* Chromium only: CSS-based headers and footers */
@page {
  @bottom-center {
    content: counter(page) " / " counter(pages);
  }
}

With wkhtmltopdf, you had to control margins via CLI flags even when @page was present:

# wkhtmltopdf: CSS @page margin is often overridden
wkhtmltopdf \
  --margin-top 0 \
  --margin-right 0 \
  --margin-bottom 0 \
  --margin-left 0 \
  --page-size A4 \
  input.html output.pdf

With Chromium-based engines, CSS @page works exactly as the spec defines:

/* This is all you need */
@page {
  size: A4;
  margin: 0;
}

Feature Support Comparison

CSS Feature wkhtmltopdf Chromium (FUNBREW PDF)
@page margin Partial (CLI overrides) Full spec support
@page margin-box (headers/footers) Not supported Supported
break-inside: avoid Partial Full support
CSS Grid Not supported Supported
Modern Flexbox Partial Full support
CSS custom properties Not supported Supported
position: sticky Not supported Supported
SVG rendering Unstable Stable
Japanese fonts OS-dependent (manual setup) Pre-installed

Page Break Control

/* wkhtmltopdf: only old properties reliably work */
.keep-together {
  page-break-inside: avoid;  /* old property */
}

/* Chromium: both old and new properties supported */
.keep-together {
  break-inside: avoid;          /* modern standard */
  page-break-inside: avoid;     /* fallback for compatibility */
}

Migration Code: wkhtmltopdf to FUNBREW PDF API

Step 1: Move CLI Margin Options to CSS @page

The first change: replace CLI margin flags with CSS.

/* Replace: --margin-top 10mm --margin-bottom 10mm --margin-left 15mm --margin-right 15mm */
@page {
  size: A4;
  margin: 10mm 15mm; /* top/bottom: 10mm, left/right: 15mm */
}

Step 2: Replace the subprocess call

curl:

# Before: wkhtmltopdf CLI
# wkhtmltopdf --page-size A4 input.html output.pdf

# After: FUNBREW PDF API
curl -X POST https://pdf.funbrew.cloud/api/pdf/generate \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<html>...</html>",
    "options": {
      "engine": "quality",
      "format": "A4"
    }
  }' \
  -o output.pdf

To read from a file and send via curl:

HTML_CONTENT=$(cat input.html)

curl -X POST https://pdf.funbrew.cloud/api/pdf/generate \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d "{
    \"html\": $(python3 -c 'import json,sys; print(json.dumps(open("input.html").read()))'),
    \"options\": { \"engine\": \"quality\", \"format\": \"A4\" }
  }" \
  -o output.pdf

Python:

import requests

# Before: wkhtmltopdf via subprocess
# import subprocess
# subprocess.run([
#     'wkhtmltopdf',
#     '--page-size', 'A4',
#     '--margin-top', '10mm',
#     '--encoding', 'utf-8',
#     'input.html', 'output.pdf'
# ])

# After: FUNBREW PDF API
with open('input.html', 'r', encoding='utf-8') as f:
    html_content = f.read()

response = requests.post(
    'https://pdf.funbrew.cloud/api/pdf/generate',
    headers={
        'Authorization': 'Bearer sk-your-api-key',
        'Content-Type': 'application/json',
    },
    json={
        'html': html_content,
        'options': {
            'engine': 'quality',
            'format': 'A4',
        },
    },
)

data = response.json()

# Download the generated PDF
pdf_response = requests.get(data['data']['download_url'])
with open('output.pdf', 'wb') as f:
    f.write(pdf_response.content)

print('PDF generated:', data['data']['download_url'])

Node.js:

const fs = require('fs');

// Before: wkhtmltopdf via child_process
// const { execSync } = require('child_process');
// execSync(`wkhtmltopdf --page-size A4 input.html output.pdf`);

// After: FUNBREW PDF API
const htmlContent = fs.readFileSync('./input.html', 'utf-8');

const response = await fetch('https://pdf.funbrew.cloud/api/pdf/generate', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk-your-api-key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    html: htmlContent,
    options: {
      engine: 'quality',
      format: 'A4',
    },
  }),
});

const result = await response.json();

// Download the generated PDF
const pdfResponse = await fetch(result.data.download_url);
const pdfBuffer = await pdfResponse.arrayBuffer();
fs.writeFileSync('./output.pdf', Buffer.from(pdfBuffer));

console.log('PDF generated:', result.data.download_url);

Japanese Font Migration

The wkhtmltopdf Font Problem

With wkhtmltopdf, rendering Japanese text required manual font installation on every Linux server:

# Debian/Ubuntu: required for Japanese to render
sudo apt-get install -y fonts-noto-cjk

# Or install manually from Google's Noto GitHub

If the fonts weren't installed — or if a server was provisioned without them — Japanese text silently broke.

Zero-Configuration Japanese in FUNBREW PDF

FUNBREW PDF has Noto Sans JP pre-installed on every server. No package installation, no Base64 embedding. Just reference the font name in your CSS:

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <style>
    body {
      /* Noto Sans JP is pre-installed. No @import or <link> needed. */
      font-family: 'Noto Sans JP', sans-serif;
      font-size: 14px;
      line-height: 1.7;
    }
    h1 { font-weight: 700; }
  </style>
</head>
<body>
  <h1>日本語タイトル</h1>
  <p>ひらがな、カタカナ、漢字、記号(①②③)がすべて正しく表示されます。</p>
</body>
</html>

For air-gapped environments requiring Base64 font embedding, see the Japanese font guide.

Common Post-Migration Issues

Issue 1: Margins Changed

Cause: wkhtmltopdf CLI margin options were not fully reflected in CSS @page. The effective margins differed from the declared CSS values.

Fix: Set @page explicitly with physical units. Use the Playground to visually verify margins:

@page {
  size: A4;
  margin: 15mm 20mm; /* 15mm top/bottom, 20mm left/right */
}

Issue 2: Page Break Positions Changed

Cause: Chromium applies break-inside: avoid more strictly than wkhtmltopdf did, which can shift where pages break.

Fix: Explicitly declare break behavior for all elements that should stay together:

.invoice-footer,
.data-card,
.section-header {
  break-inside: avoid;
  page-break-inside: avoid; /* fallback */
}

.new-chapter {
  break-before: page;
}

See the CSS tips guide for the full page break reference.

Issue 3: Headers/Footers Behavior Changed

Cause: wkhtmltopdf had --header-html and --footer-html CLI options. These don't translate directly.

Fix (CSS approach): Use @page margin boxes in Chromium:

@page {
  margin: 20mm 15mm 25mm 15mm; /* Extra bottom margin for footer */

  @bottom-center {
    content: "Page " counter(page) " of " counter(pages);
    font-size: 9pt;
    color: #6b7280;
  }

  @top-right {
    content: "Confidential";
    font-size: 8pt;
    color: #9ca3af;
  }
}

Fix (API approach): Pass headerHtml / footerHtml parameters. See the API docs for details.

Issue 4: Slightly Different Visual Output

Cause: Chromium renders CSS more precisely than wkhtmltopdf. Some CSS hacks that compensated for wkhtmltopdf quirks may produce unexpected results in Chromium.

Fix: Test in the Playground. Paste your HTML and see the exact PDF output in real time. Remove any wkhtmltopdf-specific CSS workarounds.

Migration Checklist

Use this checklist to track your migration:

□ Obtain FUNBREW PDF API key
□ Move CLI margin flags to CSS @page { margin: ...; }
□ Verify HTML encoding is UTF-8
□ Add font-family: 'Noto Sans JP', sans-serif; for Japanese text
□ Replace page-break-inside with break-inside (keep both for compatibility)
□ Test with representative HTML in the Playground
□ Run integration test with production HTML
□ Visually verify: page count, margins, fonts, page breaks
□ Remove wkhtmltopdf subprocess calls from codebase

Summary

Aspect wkhtmltopdf FUNBREW PDF (Chromium)
@page CSS Partial — CLI overrides Full spec support
margin: 0 Often ignored Works as expected
Japanese fonts Manual OS install required Pre-installed
CSS Grid / modern Flexbox Not supported Fully supported
Security patches None (EOL) Ongoing
Headers/footers CLI --header-html CSS @page or API params
JavaScript support Limited Full Chromium support

The migration path is straightforward: replace the subprocess call with an API request, move CLI margin flags to CSS @page, and optionally add font-family: 'Noto Sans JP'. Most existing HTML templates work without further changes.

Try your HTML in the Playground to verify the output before updating production code. Full API details are in the API reference.

Related

Powered by FUNBREW PDF