PDFPipe

Migrating from wicked_pdf and Snappy / Already HTML and CSS

Migrating off wicked_pdf or Snappy, the wkhtmltopdf wrappers

Language wrappers that shell out to the wkhtmltopdf binary, where the framework integration is the thing you are actually replacing and the engine change comes along with it.

What ports and what does not

Your views port, and that is more significant here than in a raw wkhtmltopdf migration, because these wrappers rendered framework templates rather than static files. The controller action that returned a PDF, the layout, the partials and the asset helpers all continue to exist: what changes is that instead of handing a rendered string to a local binary, you hand it to an HTTP call. The wrapper's own configuration does not port, and neither does its most convenient feature, which is that it resolved your asset pipeline URLs locally.

What the call becomes

Render the view to a string the way you already do, then POST that string as html to /v1/pdf. The wrapper's options hash maps onto the options object with the same names the raw binary used, because these wrappers were passing flags through. The part with no counterpart is the wrapper's handling of local assets: it could reference a stylesheet on the local filesystem, and a hosted renderer cannot, so every asset URL has to become absolute and publicly reachable, or be inlined into the markup you send.

Side by side

The old shape and the new one, with the parts that have no counterpart called out in comments rather than quietly omitted.

ruby
# Before, in a Rails controller
respond_to do |format|
  format.pdf do
    render pdf: "invoice",
           template: "invoices/show",
           page_size: "A4",
           margin: { top: 20, bottom: 25 },
           print_media_type: true
  end
end

# After
html = render_to_string(template: "invoices/show", layout: "pdf")

response = Faraday.post("https://api.pdfpipe.xyz/v1/pdf") do |req|
  req.headers["Authorization"] = "Bearer #{ENV.fetch('PDFPIPE_KEY')}"
  req.headers["Content-Type"] = "application/json"
  req.body = {
    html: html,
    filename: "invoice.pdf",
    options: {
      format: "A4",
      margin: { top: "20mm", bottom: "25mm", left: "15mm", right: "15mm" },
    },
  }.to_json
end

send_data response.body, filename: "invoice.pdf", type: "application/pdf"

# The asset problem: this stylesheet reference worked because the binary
# ran on the same machine. It will not resolve from a hosted renderer.
#   <%= wicked_pdf_stylesheet_link_tag "pdf" %>
# Inline the CSS into the string, or serve it from a public absolute URL.

The CSS delta, in both directions

What the old engine accepted that a current one does not, and what it refused that a current one wants. The second direction is the one people forget, and it is where the value of the migration is.

  • Everything in the wkhtmltopdf CSS delta applies, because that is the engine you were on. Flexbox and grid start working, float workarounds start behaving differently, and @page becomes live.
  • The wrapper's asset helpers were producing file paths or protocol-relative URLs. Both fail from a hosted renderer, and the failure is silent: the stylesheet simply does not load and the document renders with browser defaults.
  • Any CSS that depended on the asset pipeline's fingerprinting will need the fingerprinted URL to be public, which for most deployments it already is, but the check is worth doing before rather than after.

What changes without anything erroring

The dangerous list. Each of these produces a different document and no diagnostic, so none of them are caught by a test that only checks the render succeeded.

  • The whole document, if the stylesheet does not load. This is the loudest possible visual change and it produces no error at all: a fully rendered, completely unstyled document.
  • Everything in the wkhtmltopdf list: page count, fonts, headers and footers.
  • Timing. The wrapper ran synchronously in your request, so a slow render was a slow request. It still is, but now the latency includes a network call, and the timeout that governs it is a different one from the one you had configured.

What to diff before cutting over

Before anything else, take the string your view produces and check that every URL in it is absolute and publicly fetchable. That one check prevents the failure that accounts for most of these migrations going wrong. Then diff page counts and extracted text across a representative set, the same as any wkhtmltopdf migration.

The one thing that always breaks

Assets. The wrapper existed partly to solve local asset resolution, and removing it removes that solution. Stylesheets, images and fonts referenced by relative path or by a filesystem helper all stop loading at once, and because a document still comes back, the failure is discovered by looking at it rather than by an exception.

Frequently asked

Do I have to rewrite my templates to leave wicked_pdf and Snappy?

No. The templates are HTML and CSS and they carry across. What needs attention is the delta above: the places the two engines disagree, and the configuration that used to live outside the document and now lives inside it, or the other way round.

Can I run both for a while?

Yes, and it is the safest way to do it. Put both behind one internal function that takes your data and returns bytes, switch on an environment variable, and run the new path on real traffic while the old one still serves. You get a diff on real documents rather than on fixtures, and you keep a way back that does not involve a deploy.

What about the documents already generated?

Nothing here changes them. They are files that already exist. What is worth deciding before the cutover is whether a regenerated document has to match the original byte for byte or merely say the same thing, because for anything with a legal or audit character the answer is usually to keep the original file rather than to be able to reproduce it.

Other migrations

The nearest neighbours first, then others that started from the same kind of tool, because the model matters more than the language.

Render one of your existing documents through the playground before changing any code. That comparison is the whole of the risk assessment for this migration.