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_pdf | sghtmltopdf | Notes |
|---|---|---|
pdf: "name" | The same | The file name; .pdf is appended for you |
template:, layout:, locals:, formats: | The same | Passed straight through to Rails view rendering |
disposition:, filename:, status: | The same | disposition is inline by default |
show_as_html: true | The same | For debugging; returns the HTML instead of a PDF |
page_size: "A4" | page_size: "A4" | |
page_height:, page_width: | The same | Pass 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: true | The same | |
background: false | no_background: true | |
encoding: "UTF-8" | The same | |
title: | The same | PDF metadata |
user_style_sheet: | The same | An array of paths is accepted too |
no_pdf_compression: true | The same | |
cover: "shared/cover" | cover: <file path> | A path to an HTML file, not a template name; see below |
toc: {} | toc: true | Adjust 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_status | — | JavaScript is never executed; that is a deliberate non-goal |
print_media_type | — | Print media is always assumed |
lowquality, viewport_size, disable_smart_shrinking | — | Specific to WebKit |
exe_path, wkhtmltopdf | — | No external process is used |
extra | — | A 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.
HTML for the cover, header, and footer
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_pdf | sghtmltopdf |
|---|---|
wicked_pdf_stylesheet_link_tag | sghtmltopdf_stylesheet_link_tag |
wicked_pdf_image_tag | sghtmltopdf_image_tag |
wicked_pdf_asset_path | sghtmltopdf_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
@pagein CSS: in wkhtmltopdf the CLI wins, in sghtmltopdf@pagewins and the options are initial values - Range of local file references: on Rails the
--allow-pathdefault (formerly--allow, still accepted as an alias) ispublic/plus the asset pipeline load paths. If you reference a file outside those, say/usr/share/fontsorRails.root.join("tmp"), from an<img>or aurl()in@font-face, name it explicitly withSghtmltopdf.configure { |c| c.allow_path += ["/usr/share/fonts"] }. Fonts passed through the--fontoptions (gothic_fontand friends) are not subject to this restriction. - Remote fetching: retrieving
http(s)assets is off by default. Turn it on withallow_remote_assets: trueif you need it - Combining with
disable_local_file_access:--allow-pathonly narrows the range of local reads, it does not grant them. If you useddisable_local_file_access: truetogether withallow: [dir]in wicked_pdf to mean “only dir is readable”, carrying both across stops local reads entirely. Keepallow_pathon 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