# HTML to PDF in Ruby (and Rails) Convert HTML to PDF in Ruby on Rails with wicked_pdf, Grover, Ferrum, or a Net::HTTP or Faraday call to an API, plus a send_data controller. Rails apps usually generate PDFs, like invoices, receipts and reports, from an ERB view. The view part is easy. The real choice is the renderer. You can use the long-standing wkhtmltopdf, a headless Chrome driven from Ruby, or a hosted API. This guide compares them with working code and ends with a controller that needs no browser on your server. **Quick answer.** POST your HTML to the API and save the bytes. This works from any Ruby app with no gems. ```bash curl -X POST https://html-pdf.com/v1/pdf \ -H "Authorization: Bearer $HTML_PDF_KEY" \ -H "Content-Type: application/json" \ -d '{"html": "

Invoice #042

"}' \ -o invoice.pdf ``` ## Ruby HTML to PDF options This table shows what each gem renders with and what it needs on your server. | Gem | Renders with | Modern CSS | Requires | |---|---|---|---| | wicked_pdf | wkhtmltopdf (QtWebKit) | Partial, no grid | wkhtmltopdf binary (archived project) | | Grover | Puppeteer + Chrome | Yes | Node.js, Puppeteer, Chrome | | Ferrum | Chrome via DevTools protocol, pure Ruby | Yes | Chrome | | Net::HTTP / Faraday + html-pdf.com | Headless Chromium, hosted | Yes | Nothing | ## Option 1: wicked_pdf wicked_pdf has been the default Rails answer for years. It plugs into `respond_to`. ```ruby # Gemfile gem "wicked_pdf" gem "wkhtmltopdf-binary" ``` ```ruby class InvoicesController < ApplicationController def show @invoice = Invoice.find(params[:id]) respond_to do |format| format.html format.pdf do render pdf: "invoice-#{@invoice.id}", template: "invoices/show", layout: "pdf", page_size: "A4", margin: { top: 15, bottom: 15 } end end end end ``` The gem still gets releases, but the engine underneath is the problem. wkhtmltopdf's repository was archived in 2023, its WebKit is years old, and flexbox and grid layouts don't render as they do in a browser. It's fine for existing table-based templates. For anything new, see our [wkhtmltopdf alternative](https://html-pdf.com/wkhtmltopdf-alternative) guide. ## Option 2: Grover (Puppeteer) Grover calls Puppeteer from Ruby, so you get Chrome's output. ```ruby # Gemfile gem "grover" # and: npm install puppeteer ``` ```ruby html = render_to_string(template: "invoices/show", layout: "pdf", formats: [:html]) pdf = Grover.new(html, format: "A4", print_background: true, margin: { top: "15mm", bottom: "15mm" }).to_pdf ``` The trade-off is that every app server or worker now needs Node.js, Puppeteer and Chrome, and each render starts a browser process. ## Option 3: Ferrum (pure Ruby, Chrome only) Ferrum talks to Chrome over the DevTools protocol directly. There's no Node.js, but you still need Chrome installed. ```ruby require "ferrum" browser = Ferrum::Browser.new(timeout: 30) page = browser.create_page page.content = "

Invoice #042

" pdf = page.pdf(encoding: :binary, format: :A4, print_background: true) File.binwrite("invoice.pdf", pdf) browser.quit ``` Ferrum is lean and fast to start. You still own the Chrome install, its memory use under load, and keeping it updated. ## Or skip the server: one HTTP call html-pdf.com runs headless Chromium for you. You send HTML as JSON and get PDF bytes back, with Grover-quality output and only the standard library. Get a key, then open the link we email you to confirm your address. That adds 100 free credits. ```bash curl -X POST https://html-pdf.com/v1/signup -H "Content-Type: application/json" -d '{"email": "you@example.com"}' ``` ### With Net::HTTP (no gems) ```ruby require "net/http" require "json" module HtmlPdf ENDPOINT = URI("https://html-pdf.com/v1/pdf") Error = Class.new(StandardError) def self.render(html, **options) req = Net::HTTP::Post.new(ENDPOINT) req["Authorization"] = "Bearer #{ENV.fetch('HTML_PDF_KEY')}" req["Content-Type"] = "application/json" req.body = { html: html, format: "A4", margin: "15mm", max_pages: 20 }.merge(options).to_json res = Net::HTTP.start(ENDPOINT.host, ENDPOINT.port, use_ssl: ENDPOINT.scheme == "https", read_timeout: 60) { |http| http.request(req) } unless res.is_a?(Net::HTTPSuccess) err = (JSON.parse(res.body)["error"] rescue nil) || {} raise Error, "#{res.code} #{err['code']}: #{err['message']}" end res.body # binary PDF; res["X-Pages"] holds the page count end end File.binwrite("invoice.pdf", HtmlPdf.render("

Invoice #042

")) ``` ### With Faraday ```ruby conn = Faraday.new(url: "https://html-pdf.com") do |f| f.request :authorization, "Bearer", -> { ENV.fetch("HTML_PDF_KEY") } f.request :json f.options.timeout = 60 end res = conn.post("/v1/pdf", { html: html, format: "A4", margin: "15mm" }) raise "PDF failed: #{res.body}" unless res.success? pdf = res.body ``` To convert a public page instead, pass `url: "https://example.com"` instead of `html`. Add `response: "url"` to get JSON with a download link valid for 24 hours. ## Rails controller with send_data This controller renders an ERB template, converts it and sends the PDF to the browser. ```ruby class InvoicePdfsController < ApplicationController def show invoice = current_user.invoices.find(params[:invoice_id]) html = render_to_string(template: "invoices/pdf", layout: "pdf", locals: { invoice: invoice }, formats: [:html]) pdf = HtmlPdf.render(html, footer_html: '
' \ 'Page of
') send_data pdf, filename: "invoice-#{invoice.id}.pdf", type: "application/pdf", disposition: "inline" rescue HtmlPdf::Error => e Rails.logger.error(e.message) head :bad_gateway end end ``` ```ruby # config/routes.rb resources :invoices do resource :pdf, only: :show, controller: "invoice_pdfs" end ``` Use `disposition: "attachment"` to force a download. ERB's `<%= %>` escapes output, so customer data can't break the markup. **Assets.** The renderer can't reach `localhost` or private networks, so `image_tag` and `stylesheet_link_tag` need absolute public URLs (set `config.asset_host`). In development, inline CSS in a `