Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Migrating from wicked_pdf

A mapping and a set of notes for moving a Rails application from wicked_pdf, and wkhtmltopdf behind it, to the sghtmltopdf gem.

For the options themselves, see the wkhtmltopdf option mapping and Migrating from wkhtmltopdf. This page covers only what differs as seen from Rails and Ruby.

The smallest possible change

# Gemfile
- gem "wicked_pdf"
- gem "wkhtmltopdf-binary"
+ gem "sghtmltopdf"

Your controllers should keep working as they are.

def show
  respond_to do |format|
    format.pdf { render pdf: "invoice", template: "invoices/show", layout: "pdf" }
  end
end

No external process is started any more, so wicked_pdf’s exe_path, which pointed at the wkhtmltopdf binary, is no longer needed.

Where the configuration goes

# config/initializers/sghtmltopdf.rb
Sghtmltopdf.configure do |c|
  c.page_size   = "A4"
  c.margin_top  = "20mm"
  c.gothic_font = Rails.root.join("vendor/fonts/NotoSansJP-Regular.ttf")
end

This is the counterpart of wicked_pdf’s WickedPdf.config = {...}. The global configuration is merged first and the arguments to render win.

How the option names map

wicked_pdf takes nested hashes such as margin: {top: 10}, while sghtmltopdf takes a flat hash whose keys are the CLI flag names, with _ standing for -, so page_size: is --page-size. The option definitions live in one place in Rust, and the Ruby side keeps no whitelist of its own.

wicked_pdfsghtmltopdfNotes
pdf: "name"The sameThe file name; .pdf is appended for you
template:, layout:, locals:, formats:The samePassed straight through to Rails view rendering
disposition:, filename:, status:The samedisposition is inline by default
show_as_html: trueThe sameFor debugging; returns the HTML instead of a PDF
page_size: "A4"page_size: "A4"
page_height:, page_width:The samePass a string with a unit, such as "210mm"
orientation: "Landscape"The same
margin: {top: 10, bottom: 10}margin_top: "10mm", margin_bottom: "10mm"A bare number means mm in wicked_pdf, so state the unit
dpi:, zoom:The same
grayscale: trueThe same
background: falseno_background: true
encoding: "UTF-8"The same
title:The samePDF metadata
user_style_sheet:The sameAn array of paths is accepted too
no_pdf_compression: trueThe same
cover: "shared/cover"cover: <file path>A path to an HTML file, not a template name; see below
toc: {}toc: trueAdjust the appearance with toc_header_text: and friends
header: {left:, center:, right:}header_left:, header_center:, header_right:
header: {html: {template: "..."}}header_html_content: <rendered HTML>Render it with render_to_string and pass the result. The same for the footer (see below)
header: {line: true, spacing: 5, font_name:, font_size:}header_line: true, header_spacing: 5, header_font_name:, header_font_size:The footer works the same way
outline: {}PDF outlines are not supported
disable_javascript, javascript_delay, window_statusJavaScript is never executed; that is a deliberate non-goal
print_media_typePrint media is always assumed
lowquality, viewport_size, disable_smart_shrinkingSpecific to WebKit
exe_path, wkhtmltopdfNo external process is used
extraA raw command line string is not accepted; use the individual keys

Passing a key that is not supported raises Sghtmltopdf::UsageError at render time, with the reason; nothing is ignored silently.

wicked_pdf takes a Rails template name and renders it internally, whereas sghtmltopdf lets you pass HTML rendered with render_to_string straight to header_html_content / footer_html_content, with no temporary file.

def show
  header = render_to_string(template: "invoices/header", layout: false)

  render pdf: "invoice", template: "invoices/show", header_html_content: header
end

To use an existing HTML file, pass its path to header_html / footer_html. Writing a Rails template out to a temporary file, as before, also still works.

require "tempfile"

def show
  header = Tempfile.new(["header", ".html"])
  header.write(render_to_string(template: "invoices/header", layout: false))
  header.flush

  render pdf: "invoice", template: "invoices/show", header_html: header.path
ensure
  header&.close!
end

The same works for footer_html. Giving both a path and an HTML string for the same side raises Sghtmltopdf::UsageError.

cover still takes a file path. To use a Rails template as the cover, write the HTML rendered with render_to_string to a temporary file and pass its path.

View helpers

wicked_pdfsghtmltopdf
wicked_pdf_stylesheet_link_tagsghtmltopdf_stylesheet_link_tag
wicked_pdf_image_tagsghtmltopdf_image_tag
wicked_pdf_asset_pathsghtmltopdf_asset_path, which returns nil if nothing is found
wicked_pdf_javascript_include_tag— (unnecessary, since JavaScript is not executed)
wicked_pdf_asset_base64— (sghtmltopdf_image_tag with inline: true)

A plain image_tag works as it is, as long as the assets have been precompiled into public/. Rendering does not go through an HTTP server, so a URL such as /assets/… is resolved as a local file against --base-url, which defaults to Rails.root/public under Rails.

When the assets are not in public/ yet, as in development, use the helpers that look the real file up in the pipeline load path: sghtmltopdf_stylesheet_link_tag expands the CSS into a <style>, and sghtmltopdf_image_tag references the image by path (embedding it as a data: URI when it sits somewhere the engine cannot read). Where wicked_pdf_image_tag emitted a file:// URL, here the engine reads it as a local file.

A plain stylesheet_link_tag is not enough for CSS. The pipeline rewrites the url()s inside the CSS too, so with asset_host set a @font-face reference becomes an absolute HTTPS URL, which cannot be fetched and leaves the font falling back to the default. sghtmltopdf_stylesheet_link_tag points those at the file on disk while it expands the CSS.

<%= sghtmltopdf_stylesheet_link_tag "pdf" %>
<%= sghtmltopdf_image_tag "logo.png" %>

Differences in the defaults

  • Margins: wkhtmltopdf uses 10mm on all four sides, sghtmltopdf 1in (96px) on all four sides. State margin_* explicitly to keep the same look
  • CLI options versus @page in CSS: in wkhtmltopdf the CLI wins, in sghtmltopdf @page wins and the options are initial values
  • Range of local file references: on Rails the --allow-path default (formerly --allow, still accepted as an alias) is public/ plus the asset pipeline load paths. If you reference a file outside those, say /usr/share/fonts or Rails.root.join("tmp"), from an <img> or a url() in @font-face, name it explicitly with Sghtmltopdf.configure { |c| c.allow_path += ["/usr/share/fonts"] }. Fonts passed through the --font options (gothic_font and friends) are not subject to this restriction.
  • Remote fetching: retrieving http(s) assets is off by default. Turn it on with allow_remote_assets: true if you need it
  • Combining with disable_local_file_access: --allow-path only narrows the range of local reads, it does not grant them. If you used disable_local_file_access: true together with allow: [dir] in wicked_pdf to mean “only dir is readable”, carrying both across stops local reads entirely. Keep allow_path on its own.

Fonts

wkhtmltopdf depends on the system font configuration, whereas sghtmltopdf lets you name fonts with gothic_font, serif_font, and mono_font. For Japanese output it is safer to state them, so that you are not at the mercy of what a container happens to have.

Sghtmltopdf.configure do |c|
  c.gothic_font = Rails.root.join("vendor/fonts/NotoSansJP-Regular.ttf")
end

Moving the work to another process, which wicked_pdf could not do

wicked_pdf starts a wkhtmltopdf process for every request, while the sghtmltopdf gem converts inside your application process; the GVL is released during the heavy work, so other Puma threads keep running. If you still do not want to spend the application’s CPU on it, server_url delegates to a separate sghtmltopdf server process.

Sghtmltopdf.configure { |c| c.server_url = "http://pdf.internal:8080" }

Only one URL is accepted, on the assumption that load balancing happens in front of it, in nginx or a Kubernetes Service. If the server cannot be reached you get Sghtmltopdf::ServerError; it does not fall back to converting locally.

In server mode, options that take a local path, such as base_url, allow_path, and the font settings, cannot be given per request; they are only settable when the server starts. The defaults supplied by the Railtie are dropped automatically, but anything you set explicitly with configure produces a 400 and a UsageError, so move those to the server’s startup options.

Not there yet

  • Merging PDFs and PDF outlines are not supported

Streaming the output, on the other hand, is something wicked_pdf never had; pass a block to render. Combined with ActionController::Live, pages can be written to the response as they are finalised. See Ruby / Rails