LoginSignup
1

wicked_pdfの後継になる?FerrumでHTMLからPDFを生成する

Last updated at Posted at 2023-12-22

はじめに

この記事は「mofmof Advent Calendar 2023」22日目の記事です。

wicked_pdfの代替案としてFerrumというgemを使ってみました。

元々はPDF生成機能を実装するためにwicked_pdfを使おうとしていましたが、
依存しているwkhtmltopdfが今年に入ってアーカイブされていたことを知りました。
内部的に利用しているQT Webkitレンダリングエンジンのメンテナンスが止まったことで、wkhtmltopdfもメンテナンスが継続できなくなってしまったようです。

wicked_pdfの現状

wicked_pdfwkhtmltopdf-binaryと併せて利用しますが、wkhtmltopdf-binaryのメンテナンスが滞っているようです。

willnetさんがM1 Mac(arm64)対応やdebian12(bookworm)対応をしてくださったPRが出されていますが、リポジトリは管理されている様子がなく、マージされる気配も感じられません。

M1 Mac(arm64)やdebian12が使われているRubyイメージを使用していてwicked_pdfを使いたい場合は、このPRを元に公開されたwkhtmltopdf-binary-ngという別のgemを使うのが現状良さそうです。

Gemfile
gem 'wkhtmltopdf-binary-ng'

PDF生成どうするか

選択肢としては、ヘッドレスブラウザ等でHTMLをPDFに変換できるGroverFerrumといったgemがあります。

FerrumはRubyでChromeブラウザを操作できるAPIを提供してくれて、GroverのようにPuppeteerを経由する必要がありません。

今回はFerrumでPDF生成してみます。

前提

  • Rails 7.1.2
    • Rails 7.1から$ rails newでDockerfileが生成されるようになっています
    • $ rails g scaffold post title:string body:text published:boolean実行済み

必要なのはChrome or Chromiumだけ

Dockerfileでインストールしておく

Dockerfile
# Install packages needed for deployment
RUN apt-get update -qq && \
-     apt-get install --no-install-recommends -y curl libvips postgresql-client && \
+   apt-get install --no-install-recommends -y curl libvips postgresql-client chromium && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

FerrumでPDF生成

posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: %i[ show edit update destroy download_pdf ]

  # 中略
  
  def download_pdf
    html = render_to_string(template: 'posts/_post', layout: 'pdf', locals: { post: @post })
    pdf = html2pdf(html)
    send_data pdf, filename: 'post.pdf', type: 'application/pdf'
  end
  
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.find(params[:id])
    end
    
    # Ferrumを使ってHTMLからPDFを生成
    def html2pdf(html)
      # chromiumへのパスとno-sandbox browserオプションを渡す
      browser = Ferrum::Browser.new(browser_path: '/usr/bin/chromium', browser_options: { 'no-sandbox': nil })
      
      header_html = render_to_string('pdf/header', layout: false)
      footer_html = render_to_string('pdf/footer', layout: false)
      
      browser.goto("data:text/html,#{html}")
      
      pdf = browser.pdf(
        format: :A4,
        encoding: :binary,
        display_header_footer: true,
        header_template: header_html,
        footer_template: footer_html,
      )
      
      browser.quit
      
      pdf
    end
end

その他のコード
  • download_pdfアクションのルート追加
cofig/routes.rb
Rails.application.routes.draw do
  resources :posts do
    member do
      get :download_pdf
    end
  end
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "rails/health#show", as: :rails_health_check

  # Defines the root path route ("/")
  # root "posts#index"
end
  • PDFダウンロードボタンを追加
posts/show.html.erb
<p style="color: green"><%= notice %></p>

<%= render @post %>

<div>
  <%= link_to "Edit this post", edit_post_path(@post) %> |
  <%= link_to "Back to posts", posts_path %>

  <%= button_to "Destroy this post", @post, method: :delete %>
+ <%= link_to 'Download PDF', download_pdf_post_path(@post) %>
</div>
  • PDF用レイアウト
layouts/pdf.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>PDF generated by Ferrum</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
  </head>

  <body>
    <%= yield %>
  </body>
</html>
layouts/pdf/header.html.erb
<div style="margin-left: 0.5cm; font-size: 9px; width: 100%;">
  <span class="date"></span>
</div>
layouts/pdf/footer.html.erb
<div style="position: relative; border-top: 1px solid black; margin: 0.5cm; font-size: 9px; width: 100%;">
  <div style="position: absolute; width: 100%; top: 0.2cm; text-align: center;">
    <span class='pageNumber'></span> / <span class='totalPages'></span>
  </div>
  <div style="position: absolute; right: 0; top: 0.2cm;">generated by Ferrum</div>
</div>

リポジトリはこちら

フォーマット指定

標準的な用紙サイズがオプションとして用意されています。
formatオプションで用紙サイズを指定するか、paper_widthpaper_heightを指定します。

standard paper sizes :letter, :legal, :tabloid, :ledger, :A0, :A1, :A2, :A3, :A4, :A5, :A6

Dockerで利用する場合

Dockerコンテナ内で、rootユーザーとして実行する場合は以下のオプションを渡す必要があります。

Ferrum::Browser.new(browser_options: { 'no-sandbox': nil })

また、M1 Mac上のDockerコンテナ内でFerrumを実行する際にChromeプロセスがクラッシュするという報告があるみたいです。今のところ自分の環境では発生していませんが不穏ですね。

ヘッダーとフッターはカスタマイズできる

ヘッダーとフッターはHTML文字列を渡してカスタマイズできます。

display_header_footer: trueにしても表示されないと思っていましたが、極小文字サイズで見切れてしまっていただけでした...。
font-sizeだけは少なくともstyle設定する必要がありますね。

display_header_footer: trueにするとヘッダーとフッター両方表示してしまうので、どちらかだけ表示したい場合は空文字を渡します。

最後に

ここまで読んでくださり、ありがとうございました!
帳票や請求書などのPDF作成機能実装時はwkhtmltopdfに大変お世話になったので悲しいです。
お世話になっているライブラリに対して、個人で少しでも貢献できるようになりたいと強く感じますね。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1