You've built your HTML to PDF pipeline, but the output has garbled text, broken columns, tables spilling off the page, or images that simply don't appear. These problems are frustrating because the HTML looks perfect in the browser.
The root cause is almost always the same: a mismatch between how PDF rendering engines work and how your HTML or CSS is written. This guide covers the ten most common HTML to PDF problems — with Before/After code examples — when using FUNBREW PDF or any HTML to PDF tool. If you need the full picture of available conversion methods, start with the HTML to PDF Complete Guide.
Problem 1: Garbled Text and Missing Fonts
Symptoms
Text appears as boxes (□□□□), question marks, or is replaced by an unrelated character. CJK characters are especially susceptible.
Cause
The PDF rendering server doesn't have the required font installed, or CSS doesn't specify a fallback font chain that includes that language.
Fix
Before (generic font stack)
body {
font-family: sans-serif; /* No CJK font specified */
}
After (explicit font stack with CJK support)
body {
font-family: 'Noto Sans JP', 'Hiragino Sans', 'Yu Gothic', 'Meiryo', sans-serif;
}
code, pre {
font-family: 'Noto Sans Mono', 'Source Code Pro', monospace;
}
For self-hosted environments where you control the font files, embed the font as Base64:
@font-face {
font-family: 'IPAexGothic';
src: url('data:font/truetype;base64,AAEAAAA...') format('truetype');
}
body {
font-family: 'IPAexGothic', sans-serif;
}
If the rendering server has network access, Google Fonts works well:
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');
body {
font-family: 'Noto Sans JP', sans-serif;
}
FUNBREW PDF ships with Noto Sans JP pre-installed. Japanese text renders correctly without any extra configuration. For a comparison of font support across tools, see HTML to PDF API Comparison 2026.
FUNBREW PDF API example
curl -X POST https://pdf.funbrew.cloud/api/v1/pdf/generate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<style>body { font-family: \"Noto Sans JP\", sans-serif; }</style><p>日本語テキスト / Japanese text</p>",
"options": { "format": "A4" }
}' \
--output output.pdf
Problem 2: CSS Layout Breaks
Symptoms
Two-column layouts collapse to one column in the PDF. Flex containers behave unexpectedly. The PDF looks like a mobile layout even though the browser renders it correctly.
Cause
PDF engines — especially WebKit-based ones like wkhtmltopdf — don't fully support modern CSS. Viewport width assumptions also differ between browser and PDF rendering contexts.
Fix
Before (modern CSS that may break in older engines)
.container {
display: flex;
gap: 24px; /* gap is unsupported in older engines */
}
.column {
width: 50%;
}
After (table-based layout for maximum compatibility)
.container {
display: table;
width: 100%;
border-spacing: 0;
}
.column {
display: table-cell;
width: 50%;
padding-right: 12px;
vertical-align: top;
}
.column:last-child {
padding-right: 0;
padding-left: 12px;
}
When using a Chromium-based engine, flexbox works fine if you lock in the width:
@media print {
.container {
display: flex;
width: 794px; /* A4 at 96dpi */
margin: 0;
padding: 0;
}
.column {
flex: 0 0 50%;
box-sizing: border-box;
}
}
Responsive designs often switch to a mobile layout at small viewport widths. Override this in print:
@media print {
.container {
max-width: none !important;
width: 100% !important;
}
.mobile-only {
display: none !important;
}
.desktop-only {
display: block !important;
}
}
Background colors are also frequently missing. Force them with:
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
For a detailed breakdown of CSS support per engine, see wkhtmltopdf vs Chromium.
Problem 3: Page Breaks in the Wrong Place
Symptoms
A table is split mid-row. A heading is left alone at the bottom of a page. You need a section to start on a new page.
Cause
Missing break-inside or break-before declarations, or missing legacy fallback properties for older engines.
Fix
Before (no page break control)
table { width: 100%; }
/* The engine decides where to break — often badly */
After (explicit page break control)
/* Prevent elements from splitting across pages */
table, figure, .card, .invoice-item {
break-inside: avoid;
page-break-inside: avoid; /* Fallback for older engines */
}
/* Never break right after a heading */
h1, h2, h3 {
break-after: avoid;
page-break-after: avoid;
}
/* Force a new page before specific sections */
.chapter, .page-break {
break-before: page;
page-break-before: always;
}
/* Prevent orphan and widow lines */
p {
orphans: 3; /* Minimum lines at bottom of page */
widows: 3; /* Minimum lines at top of page */
}
For multi-page tables, always repeat headers and prevent row splits:
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
tr {
break-inside: avoid;
page-break-inside: avoid;
}
For invoices, keep the summary block together on one page:
<div class="invoice-body">
<table class="line-items">
<thead>
<tr><th>Item</th><th>Qty</th><th>Unit Price</th><th>Total</th></tr>
</thead>
<tbody>
<!-- line items -->
</tbody>
</table>
</div>
<!-- Keep the summary block together — no page break inside -->
<div class="invoice-summary" style="break-inside: avoid; page-break-inside: avoid;">
<table>
<tr><td>Subtotal</td><td>$1,000.00</td></tr>
<tr><td>Tax (10%)</td><td>$100.00</td></tr>
<tr><td><strong>Total</strong></td><td><strong>$1,100.00</strong></td></tr>
</table>
<p>Wire transfer: Bank of Example, Account #1234567</p>
</div>
For more page break techniques, see CSS Tips for HTML to PDF.
Problem 4: Images Don't Appear in the PDF
Symptoms
Images are blank, show a broken image icon, or display a completely different image. The issue doesn't exist in the browser.
Cause
Relative URLs or local file paths that the PDF rendering server cannot resolve.
Fix
Before (relative paths that the server can't reach)
<!-- Local file path: inaccessible from the rendering server -->
<img src="../images/logo.png" alt="Logo">
<!-- Relative URL: the server doesn't know the base URL -->
<img src="/images/logo.png" alt="Logo">
After (absolute URL)
<img src="https://example.com/images/logo.png" alt="Logo">
When network access isn't available, or you want to avoid extra HTTP requests, embed images as Base64:
import base64
def image_to_base64(path: str) -> str:
with open(path, 'rb') as f:
return base64.b64encode(f.read()).decode('utf-8')
logo_b64 = image_to_base64('logo.png')
html = f'<img src="data:image/png;base64,{logo_b64}" alt="Logo" width="200">'
const fs = require('fs');
function imageToBase64(path) {
return fs.readFileSync(path).toString('base64');
}
const logoB64 = imageToBase64('./logo.png');
const html = `<img src="data:image/png;base64,${logoB64}" alt="Logo" width="200">`;
For SVGs, embed the markup directly — no encoding needed:
<svg width="200" height="60" xmlns="http://www.w3.org/2000/svg">
<text x="10" y="40" font-size="32" font-family="Arial">FUNBREW</text>
</svg>
FUNBREW PDF API example with image
curl -X POST https://pdf.funbrew.cloud/api/v1/pdf/generate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<img src=\"https://your-cdn.example.com/logo.png\" alt=\"Logo\"><p>Content below logo</p>",
"options": { "format": "A4" }
}' \
--output output.pdf
Problem 5: Tables Overflow or Get Cut Off
Symptoms
A wide table extends beyond the right edge of the page. Content on the right side of the table is missing from the PDF.
Cause
PDF has no concept of horizontal scrolling. overflow: auto has no effect. If a table is wider than the page, the content is simply clipped.
Fix
Before (scroll works in browser, but PDF clips content)
.table-wrapper {
overflow-x: auto; /* No effect in PDF */
}
table {
min-width: 900px; /* Wider than A4 (~794px at 96dpi) */
}
After (fit the table to the page)
table {
width: 100%;
table-layout: fixed; /* Distribute column widths evenly */
word-break: break-all; /* Wrap long text within cells */
}
@media print {
table {
font-size: 8pt; /* Reduce font size for dense tables */
}
td, th {
padding: 4px 6px;
}
}
For tables with many columns, switch to landscape orientation:
curl -X POST https://pdf.funbrew.cloud/api/v1/pdf/generate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<table>...</table>",
"options": {
"format": "A4",
"landscape": true
}
}' \
--output output.pdf
Hide supplementary columns in PDF output:
@media print {
.col-notes,
.col-actions {
display: none;
}
}
Problem 6: External CSS or Styles Are Not Applied
Symptoms
The PDF ignores font styles, colors, or layout rules that look correct in the browser. External stylesheet links don't appear to load.
Cause
The PDF rendering server cannot reach the relative URL of the external CSS file, or the file is behind authentication.
Fix
Before (external CSS reference)
<!-- Rendering server may not be able to reach this -->
<link rel="stylesheet" href="/assets/style.css">
After (inline CSS)
<style>
body {
font-family: 'Noto Sans JP', 'Arial', sans-serif;
font-size: 11pt;
line-height: 1.7;
color: #1a202c;
margin: 0;
padding: 0;
}
h1 { font-size: 18pt; font-weight: 700; margin-bottom: 12pt; }
h2 { font-size: 14pt; font-weight: 700; margin-bottom: 8pt; }
.highlight { background: #fef9c3; padding: 2px 4px; }
</style>
Read and inline CSS server-side before sending to the API:
def build_html_with_styles(html_content: str, css_path: str) -> str:
with open(css_path, 'r', encoding='utf-8') as f:
css = f.read()
return f'<style>{css}</style>{html_content}'
html = build_html_with_styles('<h1>Report</h1><p>Content</p>', 'style.css')
const fs = require('fs');
function buildHtmlWithStyles(htmlContent, cssPath) {
const css = fs.readFileSync(cssPath, 'utf-8');
return `<style>${css}</style>${htmlContent}`;
}
const html = buildHtmlWithStyles('<h1>Report</h1>', './style.css');
Problem 7: Dynamic Content Missing (JavaScript-Rendered Elements)
Symptoms
Charts, graphs, or dynamically rendered UI components appear blank or empty in the PDF.
Cause
The PDF engine captures the page before JavaScript finishes rendering the content.
Fix
Before (JavaScript hasn't finished when PDF is captured)
{
"html": "<canvas id='chart'></canvas><script>renderChart();</script>",
"options": {}
}
After (signal readiness before capture)
const html = `
<canvas id="chart"></canvas>
<script>
async function main() {
await renderChart();
// Signal that rendering is complete
document.title = 'pdf-ready';
}
main();
</script>
`;
For Chart.js, disable animations — they prevent the engine from capturing the final state:
const chart = new Chart(ctx, {
type: 'bar',
data: { /* ... */ },
options: {
animation: false, // Disable animations
responsive: false, // Fix dimensions
plugins: {
legend: { display: true }
}
}
});
The most reliable approach is to generate charts as images server-side:
import matplotlib.pyplot as plt
import io, base64
def chart_to_base64(data: list) -> str:
fig, ax = plt.subplots(figsize=(6, 4))
ax.bar(range(len(data)), data)
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
buf.seek(0)
return base64.b64encode(buf.read()).decode()
chart_b64 = chart_to_base64([10, 25, 40, 35, 55])
html = f'<img src="data:image/png;base64,{chart_b64}" alt="Revenue Chart">'
Problem 8: Headers and Footers Not Appearing
Symptoms
You've set up @page margins and added header/footer elements, but the PDF renders them incorrectly — overlapping content, cut off, or completely missing.
Cause
Header and footer rendering works differently across PDF engines. Some engines use the @page margin boxes (@top-center, @bottom-center), while others require specific API options. CSS-only approaches often fail because the rendering engine doesn't support the @page margin-at rules.
Fix
Before (CSS-only header that doesn't work in most engines)
@page {
@top-center { content: "Company Name"; }
@bottom-right { content: "Page " counter(page); }
}
After (fixed-position elements for headers/footers)
<style>
@page {
margin: 25mm 15mm 25mm 15mm; /* Leave space for header/footer */
}
.page-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 15mm;
text-align: center;
font-size: 9pt;
color: #718096;
border-bottom: 0.5pt solid #e2e8f0;
padding-bottom: 2mm;
}
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 15mm;
text-align: center;
font-size: 8pt;
color: #a0aec0;
}
</style>
<div class="page-header">Company Name — Confidential</div>
<div class="page-footer">Generated by FUNBREW PDF</div>
<div class="content">
<!-- Main content here -->
</div>
For page numbers, use the FUNBREW PDF API's built-in header/footer options:
curl -X POST https://pdf.funbrew.cloud/api/v1/pdf/generate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Report</h1><p>Content...</p>",
"options": {
"format": "A4",
"margin": { "top": "25mm", "bottom": "25mm" },
"displayHeaderFooter": true,
"headerTemplate": "<div style=\"font-size:9px; text-align:center; width:100%;\">Confidential Report</div>",
"footerTemplate": "<div style=\"font-size:9px; text-align:center; width:100%;\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
}
}' \
--output output.pdf
Problem 9: PDF Generation Timeout or Slow Performance
Symptoms
The API request takes over 30 seconds and eventually times out, or the PDF is slow to generate even for simple pages.
Cause
Large images, external resource loading (fonts, stylesheets, images from slow CDNs), complex JavaScript execution, or excessively large HTML payloads.
Fix
Reduce image sizes before embedding
from PIL import Image
import io, base64
def optimize_image(path: str, max_width: int = 800, quality: int = 80) -> str:
"""Resize and compress an image before Base64 embedding."""
img = Image.open(path)
if img.width > max_width:
ratio = max_width / img.width
img = img.resize((max_width, int(img.height * ratio)), Image.LANCZOS)
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=quality, optimize=True)
return base64.b64encode(buf.getvalue()).decode()
Inline all external resources
import re, requests, base64
def inline_external_resources(html: str) -> str:
"""Replace external image URLs with Base64-encoded inline data."""
def replace_src(match):
url = match.group(1)
if url.startswith("data:"):
return match.group(0)
try:
resp = requests.get(url, timeout=10)
content_type = resp.headers.get("content-type", "image/png")
b64 = base64.b64encode(resp.content).decode()
return f'src="data:{content_type};base64,{b64}"'
except Exception:
return match.group(0)
return re.sub(r'src="(https?://[^"]+)"', replace_src, html)
Set appropriate timeouts and reduce payload size
// Compress HTML by removing unnecessary whitespace
function compressHtml(html) {
return html.replace(/\s+/g, ' ').replace(/>\s+</g, '><').trim();
}
const response = await axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{ html: compressHtml(html), options: { format: 'A4' } },
{ timeout: 60000 } // Set a generous but bounded timeout
);
Problem 10: PDF File Size Too Large
Symptoms
A single-page PDF is several megabytes. Batch-generated PDFs consume excessive storage. Email attachments exceed size limits.
Cause
High-resolution embedded images (especially uncompressed PNGs), embedded fonts, or redundant CSS.
Fix
Compress images before embedding
Use JPEG instead of PNG for photographs, and SVG for logos and icons:
# Use JPEG for photos (much smaller than PNG)
img.save(buf, format="JPEG", quality=75, optimize=True)
# Use SVG for logos and simple graphics (no Base64 needed)
logo_svg = '<svg width="200" height="60">...</svg>'
Limit image resolution for print
img {
max-width: 100%;
height: auto;
image-rendering: optimizeQuality; /* Let the engine handle DPI scaling */
}
Post-process: compress the generated PDF
import subprocess
def compress_pdf(input_path: str, output_path: str):
"""Use Ghostscript to compress a PDF file."""
subprocess.run([
"gs", "-sDEVICE=pdfwrite",
"-dCompatibilityLevel=1.4",
"-dPDFSETTINGS=/ebook", # Good quality, smaller size
"-dNOPAUSE", "-dBATCH",
f"-sOutputFile={output_path}",
input_path,
], check=True)
# /screen = 72 dpi (smallest, low quality)
# /ebook = 150 dpi (good balance)
# /printer = 300 dpi (high quality)
# /prepress = 300 dpi (highest quality)
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
async function compressPdf(inputPath, outputPath) {
await execAsync(
`gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -dNOPAUSE -dBATCH -sOutputFile=${outputPath} ${inputPath}`
);
}
Debugging Workflow
Use this step-by-step approach to isolate the cause of any PDF issue.
Step 1: Check browser print preview
Most problems reproduce in the browser's print preview, which is the fastest way to diagnose them.
- Open the HTML in Chrome
- Press
Ctrl+Shift+P(Mac:Cmd+Shift+P) to open the command palette - Type "Emulate CSS media type" → select "print"
- Open Print Preview with
Ctrl+P
If the issue appears here, it's a CSS problem. If print preview looks correct, suspect a rendering engine issue.
Step 2: Test in the Playground
Paste your HTML into FUNBREW PDF's Playground for instant PDF output — no API key required.
Step 3: Compare engines
FUNBREW PDF offers two engines: fast (WebKit-based) and quality (Chromium-based).
# Test with the fast engine (wkhtmltopdf-based)
curl -X POST https://pdf.funbrew.cloud/api/v1/pdf/generate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<p>Test</p>", "options": {"engine": "fast"}}' \
--output test-fast.pdf
# Test with the quality engine (Chromium-based)
curl -X POST https://pdf.funbrew.cloud/api/v1/pdf/generate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<p>Test</p>", "options": {"engine": "quality"}}' \
--output test-quality.pdf
If the outputs differ, you've found a CSS compatibility issue. See wkhtmltopdf vs Chromium for specifics.
Step 4: Create a minimal reproduction
Strip the HTML down to the smallest snippet that still reproduces the problem.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
/* Only the relevant styles */
body { font-family: 'Noto Sans JP', sans-serif; }
</style>
</head>
<body>
<!-- Minimal element that triggers the issue -->
<p>Test: Hello World / 日本語</p>
</body>
</html>
Step 5: Check error responses
import requests
response = requests.post(
"https://pdf.funbrew.cloud/api/v1/pdf/generate",
headers={"X-API-Key": "your-api-key"},
json={"html": html_content, "options": {}},
)
if response.status_code != 200:
print(f"HTTP {response.status_code}")
print(response.json()) # Read the error message
else:
with open("output.pdf", "wb") as f:
f.write(response.content)
For a comprehensive guide to error handling and retry logic, see the PDF API Error Handling Guide.
Quick Reference
| Problem | Common Cause | Fix |
|---|---|---|
| Garbled text (tofu boxes) | Missing font on rendering server | Specify full font stack; use pre-installed fonts |
| Layout breaks | CSS incompatibility, wrong viewport | Table layout; @media print with fixed widths |
| Wrong page breaks | Missing break-inside |
break-inside: avoid + orphans/widows |
| Images missing | Relative URLs or inaccessible paths | Use absolute URLs or Base64 inline embedding |
| Table overflow | Width exceeds page | table-layout: fixed + word-break: break-all |
| Styles not applied | External CSS unreachable | Inline all CSS before sending to the API |
| Dynamic content missing | JS not finished before capture | Server-side image generation |
| Headers/footers missing | @page margin-at rules unsupported |
position: fixed or API header/footer options |
| Slow generation/timeout | Large images, external resources | Optimize images, inline resources, compress HTML |
| File size too large | Uncompressed images, embedded fonts | JPEG instead of PNG, SVG for icons, Ghostscript post-processing |
Summary
Almost every HTML to PDF issue comes down to one of three things: fonts aren't available on the server, CSS isn't compatible with the rendering engine, or resources (images, stylesheets) can't be accessed by the server.
- Garbled text: Specify an explicit font stack. FUNBREW PDF includes Noto Sans JP out of the box
- Layout issues: Inline your CSS; use table-based layouts for maximum compatibility
- Page breaks: Add
break-inside: avoidandorphans/widowswhere needed - Missing resources: Use absolute URLs or Base64 embedding for all images and fonts
- Headers/footers: Use
position: fixedor the API's built-in header/footer template options - Slow generation: Optimize and inline images before sending; compress HTML whitespace
- Large file size: Use JPEG for photos, SVG for graphics; post-process with Ghostscript if needed
- Debugging: Browser print preview first, then Playground, then minimal reproduction
For deep dives into specific areas, see CSS Tips for HTML to PDF and the wkhtmltopdf vs Chromium comparison. Start testing in the Playground — it's the fastest way to confirm whether your HTML produces the PDF you expect.
Related
- HTML to PDF Complete Guide 2026 — Overview of all conversion methods
- CSS Tips for HTML to PDF — Page breaks, margins, and font design in depth
- wkhtmltopdf vs Chromium — Engine-specific CSS compatibility
- PDF API Error Handling Guide — Error handling and retry strategies
- PDF API Quickstart by Language — Node.js, Python, PHP code examples
- HTML to PDF API Comparison 2026 — Feature and pricing comparison across services
- API Documentation — Full FUNBREW PDF option reference
- Playground — Test your HTML and get PDF output instantly
- Use Cases — Invoices, reports, certificates, and more
- Pricing — Free plan to production plans