A high-performance, concurrent HTML to PDF conversion service built with Go and the getevo/evo/v2 framework. This service provides a simple REST API to convert HTML (with CSS and JavaScript) into PDF documents using wkhtmltopdf.
Docker Image: ghcr.io/getevo/go-pdf:latest
Imagine you need to generate an invoice or receipt in your application. What do you do?
Option 1: Create messy, unreadable code embedded in your application that requires recompiling the entire app for each design change?
Option 2: Use an internal service that takes an HTML template and generates PDFs for you?
This is why go-pdf matters! It lets you painlessly generate any PDF with your custom style. Just design your document as HTML/CSS (like you would for a webpage), send it to this service, and get back a professional PDF. No more wrestling with complex PDF libraries or maintaining rigid PDF generation code in your main application.
- 🎨 Design PDFs like webpages: Use familiar HTML/CSS
- 🔄 Iterate quickly: Change designs without recompiling your app
- 📦 Keep it separate: PDF generation logic stays out of your main codebase
- 🚀 Scale independently: Deploy PDF service separately from your application
- 🎯 Reuse templates: Same HTML template for web display and PDF generation
- Fast & Efficient: Built with Go for high performance and low memory footprint
- Concurrent Processing: Handles multiple concurrent PDF generation requests
- REST API: Simple POST endpoint for PDF generation with configurable options
- Automatic Cleanup: Automatically removes cached files older than 1 hour
- Docker Support: Multi-stage Dockerfile for minimal image size
- No Database Required: Lightweight service with no database dependencies
- Framework-based: Uses getevo/evo/v2 framework for robust HTTP handling
- Flexible Configuration: Control page size, quality, JavaScript execution, and more via query parameters
go-pdf is a microservice that converts HTML documents (including CSS and JavaScript) into PDF files. It uses wkhtmltopdf under the hood, which renders HTML using the WebKit engine and generates high-quality PDF output.
The service temporarily stores HTML files and generated PDFs in a cache directory, then automatically cleans up files older than 1 hour to prevent disk space issues.
- Generate invoices and receipts from HTML templates
- Create PDF reports from web content
- Convert HTML emails to PDF
- Generate printable documents from web applications
- Batch PDF generation from dynamic HTML content
Converts HTML content to PDF and returns the generated PDF file.
- Method: POST
- Content-Type: text/html
- Body: Raw HTML content (string)
- Content-Type: application/pdf
- Headers:
Content-Disposition: attachment; filename={hash}.pdfX-Generated-At: Timestamp of generation
- Body: PDF file (binary)
All query parameters are optional and allow you to control the PDF generation behavior:
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
js_delay |
integer | 0 |
5000 |
JavaScript delay in milliseconds before rendering. Use this if your HTML contains JavaScript that needs time to execute |
image_dpi |
integer | - | - | Set the DPI for images (e.g., 300 for high quality) |
image_quality |
integer | - | 100 |
JPEG image quality (0-100, where 100 is best quality) |
lowquality |
boolean | false |
- | Use true to generate lower quality PDFs (smaller file size) |
page_height |
string | - | - | Page height (e.g., 297mm, 11.69in) |
page_width |
string | - | - | Page width (e.g., 210mm, 8.27in) |
page_size |
string | - | - | Page size preset (e.g., A4, Letter, Legal) |
enable_forms |
boolean | false |
- | Use true to enable HTML forms in the PDF |
enable_smart_shrinking |
boolean | false |
- | Use true to enable smart content shrinking to fit page |
margin_top |
string | - | - | Top margin (e.g., 10mm, 0.5in) |
margin_bottom |
string | - | - | Bottom margin (e.g., 10mm, 0.5in) |
margin_left |
string | - | - | Left margin (e.g., 10mm, 0.5in) |
margin_right |
string | - | - | Right margin (e.g., 10mm, 0.5in) |
orientation |
string | - | - | Page orientation: portrait or landscape |
Example with query parameters:
# Generate PDF with JavaScript delay and custom page size
curl -X POST "http://localhost:8080/api/v1/generate?js_delay=2000&page_size=A4&image_quality=95" \
-H "Content-Type: text/html" \
-d '<html><body><h1>High Quality PDF</h1></body></html>' \
-o output.pdf# Generate PDF with custom page dimensions
curl -X POST "http://localhost:8080/api/v1/generate?page_width=210mm&page_height=297mm" \
-H "Content-Type: text/html" \
-d '<html><body><h1>Custom Size PDF</h1></body></html>' \
-o custom.pdf# Generate low quality PDF (smaller file size)
curl -X POST "http://localhost:8080/api/v1/generate?lowquality=true&image_quality=50" \
-H "Content-Type: text/html" \
-d '<html><body><h1>Compressed PDF</h1></body></html>' \
-o compressed.pdf# Generate landscape PDF with custom margins
curl -X POST "http://localhost:8080/api/v1/generate?orientation=landscape&margin_top=20mm&margin_bottom=20mm&margin_left=15mm&margin_right=15mm" \
-H "Content-Type: text/html" \
-d '<html><body><h1>Landscape Report</h1><p>This is a wide format document.</p></body></html>' \
-o landscape.pdfcurl -X POST http://localhost:8080/api/v1/generate \
-H "Content-Type: text/html" \
-d '<html><body><h1>Hello PDF!</h1></body></html>' \
-o output.pdfWith styled HTML:
curl -X POST http://localhost:8080/api/v1/generate \
-H "Content-Type: text/html" \
-d '<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style>
body { font-family: Arial; margin: 40px; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>Hello PDF!</h1>
<p>This is a test document.</p>
</body>
</html>' \
-o output.pdfconst html = `
<!DOCTYPE html>
<html>
<head>
<title>Invoice</title>
<style>
body { font-family: Arial, sans-serif; }
.header { background-color: #4CAF50; color: white; padding: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>Invoice #12345</h1>
</div>
<p>Thank you for your purchase!</p>
</body>
</html>
`;
fetch('http://localhost:8080/api/v1/generate', {
method: 'POST',
headers: {
'Content-Type': 'text/html'
},
body: html
})
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'invoice.pdf';
a.click();
});import requests
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>Monthly Report</h1>
<p>This is the monthly report generated on demand.</p>
</body>
</html>
"""
response = requests.post(
'http://localhost:8080/api/v1/generate',
headers={'Content-Type': 'text/html'},
data=html_content
)
if response.status_code == 200:
with open('report.pdf', 'wb') as f:
f.write(response.content)
print('PDF generated successfully!')
else:
print(f'Error: {response.status_code}')Generates QR codes in various formats (SVG by default, or PNG). Supports multiple QR code types including URLs, WiFi networks, contact cards (vCard/MeCard), geographic coordinates, calendar events, and more.
- Method: GET
- Parameters: All parameters are passed via query string
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | No | text |
QR code type: text, url, wifi, email, sms, phone, vcard, mecard, geo, event, deeplink |
content |
string | Yes* | - | Main content (varies by type) |
format |
string | No | svg |
Output format: png or svg |
size |
integer | No | 256 |
Image size in pixels (64-2048, sets both width and height) |
password |
string | No | - | For WiFi type: network password |
latitude |
float | No | - | For geo type: latitude coordinate |
longitude |
float | No | - | For geo type: longitude coordinate |
label |
string | No | - | For geo type: location label/name |
first_name |
string | No | - | For vcard/mecard: first name |
last_name |
string | No | - | For vcard/mecard: last name |
organization |
string | No | - | For vcard/mecard: organization name |
phone |
string | No | - | For vcard/mecard/phone/sms: phone number |
email |
string | No | - | For vcard/mecard/email: email address |
address |
string | No | - | For vcard/mecard: physical address |
website |
string | No | - | For vcard: website URL |
note |
string | No | - | For vcard: additional notes |
summary |
string | No | - | For event: event title |
description |
string | No | - | For event: event description |
location |
string | No | - | For event: event location |
start_time |
string | No | - | For event: start time (RFC3339 or YYYYMMDDTHHMMSS) |
end_time |
string | No | - | For event: end time (RFC3339 or YYYYMMDDTHHMMSS) |
subject |
string | No | - | For email type: email subject (query parameter) |
body |
string | No | - | For email/sms type: message body (query parameter) |
* Required for most types, except vcard/mecard (where name fields can be used) and geo (where lat/lng can be used)
- Content-Type:
image/svg+xml(default) orimage/png - Headers:
Content-Disposition: inline; filename=qrcode-{type}.{format}X-QR-Type: The QR code type that was generatedX-QR-Format: The output format (png or svg)X-Generated-At: Timestamp (RFC3339)
- Body: SVG or PNG image file
- Text/Plaintext (
type=text): Plain text QR code - URL (
type=url): Web URLs (auto-adds https:// if missing) - WiFi (
type=wifi): WiFi network credentials (requirescontentas SSID, optionalpassword) - Email (
type=email): Email addresses (supportssubjectandbodyquery parameters) - SMS (
type=sms): SMS messages (supportsbodyquery parameter) - Phone (
type=phone): Phone numbers for dialing - vCard (
type=vcard): Contact cards in vCard 3.0 format - MeCard (
type=mecard): Simplified contact format - Geo Coordinates (
type=geo): Geographic coordinates (uselatitude/longitudeorcontentas "lat,lng") - Event (
type=event): Calendar events in iCalendar format - Deep Link (
type=deeplink): App deep links (custom URL schemes)
SVG QR Code (default):
curl "http://localhost:8080/api/v1/qrcode?type=text&content=Hello%20World&size=256" \
-o qrcode.svgPNG QR Code with custom size:
curl "http://localhost:8080/api/v1/qrcode?type=url&content=https://www.google.com&format=png&size=512" \
-o qrcode.pngWiFi QR Code:
curl "http://localhost:8080/api/v1/qrcode?type=wifi&content=MyNetwork&password=MyPassword123" \
-o wifi-qr.svgvCard QR Code:
curl "http://localhost:8080/api/v1/qrcode?type=vcard&first_name=John&last_name=Doe&email=john@example.com&phone=%2B1234567890&organization=Example%20Corp" \
-o contact.svgGeo Coordinates QR Code:
curl "http://localhost:8080/api/v1/qrcode?type=geo&latitude=40.7128&longitude=-74.0060&label=New%20York%20City" \
-o location.svgJavaScript Example:
// Generate SVG QR code (default)
const url = 'http://localhost:8080/api/v1/qrcode?type=url&content=https://example.com&size=256';
fetch(url)
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
});
// Generate PNG QR code
const pngUrl = 'http://localhost:8080/api/v1/qrcode?type=text&content=Hello&format=png&size=512';
fetch(pngUrl)
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'qrcode.png';
a.click();
});PowerShell Example:
# SVG QR code (default)
Invoke-RestMethod -Uri "http://localhost:8080/api/v1/qrcode?type=text&content=Hello%20World&size=256" `
-Method Get -OutFile "qrcode.svg"
# PNG QR code with size 512
Invoke-RestMethod -Uri "http://localhost:8080/api/v1/qrcode?type=url&content=https://example.com&format=png&size=512" `
-Method Get -OutFile "qrcode.png"Python Example:
import requests
# SVG QR code (default)
response = requests.get(
'http://localhost:8080/api/v1/qrcode',
params={
'type': 'text',
'content': 'Hello World',
'size': 256
}
)
with open('qrcode.svg', 'wb') as f:
f.write(response.content)
# PNG QR code
response = requests.get(
'http://localhost:8080/api/v1/qrcode',
params={
'type': 'url',
'content': 'https://example.com',
'format': 'png',
'size': 512
}
)
with open('qrcode.png', 'wb') as f:
f.write(response.content)Health check endpoint for monitoring service status and readiness.
Response (HTTP 200 - Healthy):
{
"status": "healthy",
"service": "go-pdf",
"wkhtmltopdf": "wkhtmltopdf 0.12.6 (with patched qt)\n",
"cached_files": 4,
"timestamp": "2025-10-28T00:00:00Z"
}Response (HTTP 503 - Unhealthy):
{
"status": "unhealthy",
"message": "wkhtmltopdf is not available",
"error": "exec: \"wkhtmltopdf\": executable file not found"
}Example Usage:
# Check service health
curl http://localhost:8080/health# Python health check
import requests
response = requests.get('http://localhost:8080/health')
if response.status_code == 200:
print('Service is healthy')
print(f"Cached files: {response.json()['cached_files']}")
else:
print('Service is unhealthy')- Docker Desktop installed on your PC
- Docker daemon running
Pull and run the pre-built image from GitHub Container Registry:
docker run -d -p 8080:8080 --name go-pdf ghcr.io/getevo/go-pdf:latestThat's it! The service will be available at http://localhost:8080
With docker-compose:
version: '3.8'
services:
go-pdf:
image: ghcr.io/getevo/go-pdf:latest
container_name: go-pdf
ports:
- "8080:8080"
restart: unless-stoppedThen run:
docker-compose up -d-
Clone the repository:
git clone https://github.com/getevo/go-pdf.git cd go-pdf -
Build the Docker image:
docker build -t go-pdf:latest . -
Run the container:
docker run -d -p 8080:8080 --name go-pdf go-pdf:latest
-
Verify the service is running:
curl -X POST http://localhost:8080/api/v1/generate \ -H "Content-Type: text/html" \ -d '<html><body><h1>Test</h1></body></html>' \ -o test.pdf
-
View logs:
docker logs go-pdf
-
Stop the container:
docker stop go-pdf
-
Clone and start:
git clone https://github.com/getevo/go-pdf.git cd go-pdf docker-compose up -d -
View logs:
docker-compose logs -f
-
Stop the service:
docker-compose down
The service is configured via config.yml. Key settings:
- HTTP.Port: 8080 (default) - The port the service listens on
- HTTP.BodyLimit: 50mb - Maximum request body size
- HTTP.ReadTimeout: 30s - Request read timeout
- HTTP.WriteTimeout: 60s - Response write timeout
- Database.Enabled: false - Database is disabled
You can override configuration using environment variables or by modifying the config.yml file before building the Docker image.
go-pdf/
├── main.go # Application entry point
├── config.yml # Configuration file
├── apps/
│ └── pdf/
│ ├── app.go # App registration and routing
│ └── controller.go # PDF generation logic
├── cache/ # Temporary file storage (auto-created)
├── Dockerfile # Multi-stage Docker build
├── docker-compose.yml # Docker Compose configuration
└── README.md # This file
- Request: Client sends HTML content via POST to
/api/v1/generate - Validation: Service validates the HTML content
- File Creation: HTML is written to a temporary file in the cache directory
- Conversion: wkhtmltopdf converts HTML to PDF
- Response: PDF is read and sent back to the client
- Cleanup: Files older than 1 hour are automatically deleted every 10 minutes
- Concurrent Requests: The service is designed to handle concurrent requests efficiently
- File Caching: Temporary files are cached for 1 hour to balance between reusability and disk space
- Memory Usage: The service uses minimal memory thanks to Go's efficiency and streaming responses
- Image Size: Multi-stage Docker build results in a minimal image size (~400MB including wkhtmltopdf dependencies)
Check logs:
docker logs go-pdf- Verify HTML is valid
- Check if wkhtmltopdf is installed in the container:
docker exec go-pdf wkhtmltopdf --version
Change the port mapping:
docker run -d -p 9090:8080 --name go-pdf go-pdf:latestgo mod download
go build -o go-pdf .
./go-pdf# Create a test HTML file
echo '<!DOCTYPE html><html><body><h1>Test</h1></body></html>' > test.html
# Generate PDF
curl -X POST http://localhost:8080/api/v1/generate \
-H "Content-Type: application/json" \
-d "{\"html\":\"$(cat test.html)\"}" \
-o test.pdfMIT License - See LICENSE file for details.
This project is released under the MIT License, which is one of the most permissive open-source licenses. You are free to:
- Use commercially
- Modify
- Distribute
- Use privately
The only requirement is to include the original copyright and license notice in any copy of the software/source.
- Built with getevo/evo/v2
- Uses wkhtmltopdf for HTML to PDF conversion
- HTTP framework: Fiber