weasy_pdf

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 a sed over 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:

# config/initializers/weasy_pdf.rb
WeasyPDF.configure do |config|
  config.default_options = {
    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:

  1. Vite manifest (if vite_ruby loaded):

| 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/ |

  1. Disk fallback — searches public/vite/assets/, public/assets/, public/.
  2. base_url fallback — prepends base_url to the asset path.
  3. 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 A0A10 (default: A4)
ISO B B0B10
ISO C C0C10
US Letter, Legal, Ledger
JIS JIS-B0JIS-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