WeasyPDF

Generate PDFs from Rails views using WeasyPrint — modern CSS, no JavaScript, no Chromium.
gem 'weasy_pdf'
Migrating from
wicked_pdf? Jump to Migration — typically a one-line Gemfile change plus asedover your views.
Requirements
- Ruby >= 3.2
- Rails >= 7.1
- WeasyPrint installed on the system:
apt install weasyprint # Debian/Ubuntu
brew install weasyprint # macOS
pip install weasyprint # latest version
Quick start
class InvoicesController < ApplicationController
def show
@invoice = Invoice.find(params[:id])
render pdf: "invoice_#{@invoice.number}", template: 'invoices/show'
end
end
No initializer, no ApplicationController changes — the Railtie wires everything automatically.
Configuration
Optional — sensible defaults are used if you skip this. The fastest way to scaffold an initializer with all options documented:
bin/rails generate weasy_pdf:install
Or write it yourself:
# config/initializers/weasy_pdf.rb
WeasyPDF.configure do |config|
config. = {
page_size: 'A4',
margin_top: '15mm',
media_type: 'print',
}
# base_url prepends to relative URLs in the rendered HTML when no asset
# can be resolved from disk. Useful in development.
config.base_url = Rails.env.development? ? 'http://localhost:3000' : nil
config.timeout = 60
end
Usage
Controllers
render pdf: 'invoice',
template: 'invoices/show',
layout: 'pdf',
page_size: 'A4',
margin: { top: 15, bottom: 15 },
header: { right: '[page] of [topage]', font_size: 9 },
show_as_html: params.key?('debug')
Jobs / Mailers
pdf = render_to_string pdf: 'invoice', template: 'invoices/show', layout: 'pdf'
Direct instantiation
WeasyPDF.new.pdf_from_string('<h1>Hello</h1>')
WeasyPDF.new.pdf_from_html_file('/path/to/file.html')
WeasyPDF.new.pdf_from_url('https://example.com')
PDF layout
<!DOCTYPE html>
<html>
<head>
<%= weasy_pdf_stylesheet_link_tag 'pdf' %>
</head>
<body>
<%= weasy_pdf_image_tag 'logo.png', width: 150 %>
<%= yield %>
</body>
</html>
Asset helpers
<%= weasy_pdf_stylesheet_link_tag 'pdf' %>
<%= weasy_pdf_image_tag 'logo.png', width: 150 %>
<%= weasy_pdf_asset_path 'logo.png' %>
<%= weasy_pdf_asset_base64 'logo.png' %>
<%= weasy_pdf_url_base64 'https://example.com/img.png' %>
Asset support
Vite-first (auto-detected when vite_ruby is present) with fallback to precompiled assets from public/.
Each asset is resolved in this order:
- Vite manifest (if
vite_rubyloaded):
| Environment | Behavior |
|---|---|
| Production CDN | <link href="https://cdn.../pdf-abc.css"> — WeasyPrint downloads it |
| Dev + Vite server running | Inlines CSS via HTTP from localhost:5173 |
| No Vite server (CI/test) | Reads from public/vite/assets/ |
- Disk fallback — searches
public/vite/assets/,public/assets/,public/. base_urlfallback — prependsbase_urlto the asset path.- Returns input unchanged — logs a warning so misconfigurations are visible.
Headers and footers
CSS @page margin boxes — text and counters only:
render pdf: 'report',
header: { left: 'Acme', center: 'Confidential', right: '[page] of [topage]' },
footer: { center: '[page]', font_size: 8 }
| Token | CSS output |
|---|---|
[page] |
counter(page) |
[topage] |
counter(pages) |
[section], [title], [date], [time], [webpage] |
Removed — no CSS equivalent |
For complex headers (logos, styled HTML), use WeasyPrint running elements:
.pdf-header { position: running(header); }
@page { @top-center { content: element(header); } }
<div class="pdf-header">
<img src="<%= weasy_pdf_asset_path('logo.png') %>" height="30">
<span>Invoice #<%= @invoice.number %></span>
</div>
Page sizes and orientation
page_size: accepts any CSS Paged Media keyword:
| Family | Sizes |
|---|---|
| ISO A | A0 … A10 (default: A4) |
| ISO B | B0 … B10 |
| ISO C | C0 … C10 |
| US | Letter, Legal, Ledger |
| JIS | JIS-B0 … JIS-B5 |
Custom dimensions (numbers default to mm; strings pass through):
render pdf: 'receipt', page_width: 80, page_height: 200 # 80mm × 200mm
render pdf: 'badge', page_width: '4in', page_height: '6in' # any CSS unit
Orientation:
render pdf: 'report', page_size: 'A4', orientation: 'Landscape'
page_width / page_height take precedence over page_size / orientation.
Rack Middleware
# config/application.rb
config.middleware.use WeasyPDF::Middleware
config.middleware.use WeasyPDF::Middleware, { page_size: 'A4' }, only: [/^\/invoices/]
Requests to /invoices/1.pdf are transparently rendered as PDF.
Limitations
| JavaScript execution | Not supported — WeasyPrint is JS-free. Render server-side first. |
| HTML template headers/footers | Not supported — raises WeasyPDF::Error pointing to running elements |
Webpacker / Shakapacker *_pack_tag helpers |
Not supported — no equivalents |
pdf_from_url |
Basic HTTP only — no auth, no JS rendering |
outline, dpi, proxy, cookie, extra |
Accepted silently (migration compat), not passed to WeasyPrint |
Migration from Wicked PDF
Not migrating from
wicked_pdf? You can stop reading here.
Why migrate
| wicked_pdf | weasy_pdf | |
|---|---|---|
| Backend | wkhtmltopdf (archived 2023) | WeasyPrint (active) |
| CSS engine | WebKit 2013 — no flexbox/grid | Modern CSS |
| RAM per worker | ~75–200MB + known leaks | ~35–80MB, clean exit |
| Vite | Manual workarounds | Native (auto-detected) |
| Runtime deps | activesupport, ostruct |
activesupport |
Steps
1. Gemfile
- gem 'wicked_pdf'
- gem 'wkhtmltopdf-binary'
+ gem 'weasy_pdf'
2. Initializer
mv config/initializers/wicked_pdf.rb config/initializers/weasy_pdf.rb
sed -i 's/WickedPdf/WeasyPDF/g' config/initializers/weasy_pdf.rb
3. Bulk-rename helpers in views
find app/views -name '*.erb' | xargs sed -i 's/wicked_pdf_/weasy_pdf_/g'
4. Delete dead helper calls
After step 3 your views may contain weasy_pdf_javascript_* or weasy_pdf_*_pack_tag calls
that don't exist in weasy_pdf and would raise NoMethodError at render time:
grep -rE 'weasy_pdf_(javascript_|.*_pack_)' app/views
WeasyPrint doesn't execute JavaScript and Webpacker isn't supported — removing them is correct.
5. HTML template headers
header: { html: { template: ... } } now raises WeasyPDF::Error with a migration hint.
Replace with running elements.
6. Controllers — no changes
render pdf:, render_to_string pdf:, all PDF options work identically.
7. Dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends \
weasyprint fonts-dejavu-core fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
Performance differences
Neither backend is universally faster — pick by workload:
- Long-running Sidekiq workers → weasy_pdf (no RAM leaks)
- High-concurrency web requests → weasy_pdf (lower per-process RAM)
- Documents using flexbox / grid / modern CSS → weasy_pdf (wkhtmltopdf can't render them)
- One-shot CLI on simple HTML → roughly comparable, document complexity dominates
License
MIT