最近の開発では、データをPDF化するニーズが高まっているようです。

PDF化の作業は、テンプレート構成によって大きく3つのパターンに分けられます。

  • ファイルの内容がほぼテキストで、画像や図が少ない
  • 決まったフォーマットで、空白の箇所にテキストを入れるだけ
  • フォーマットが決まっておらず、画像や図が多い

PDFを生成する有料ツールはいくつかありますが、本記事では無料ツールを使用して質の高いPDFファイルを作る方法をご紹介します。

HTMLからPDF化

HTMLからPDFを生成してくれるRuby gemはたくさんありますが、The Ruby Toolboxを見る限りではwicked_pdfpdfkitなどがよく使われているようです。
自分はwicked_pdfに慣れているので、プロジェクトにはwicked_pdfを採用しました。

wicked_pdfwkhtmltopdfHTMLからPDFを生成するツール)のRubyラッパーなので、利用する際にはwkhtmltopdfをインストールする必要があります。

Gemfileに以下の2つのgemを入れて、bundle installを実行すれば、初期インストールはひとまず完了です。

gem 'wicked_pdf'
gem 'wkhtmltopdf-binary'

ビューをPDF形式でレンダリングする

例えば、`/posts/1.pdf`のようなURLでユーザーにPDFをダウンロードさせたい場合、コントローラーを以下のように定義できます。

class PostsController < ApplicationController
  def show
    @post = Post.find params[:id]
    respond_to do |format|
      format.html
      format.pdf do
        render pdf: "post-#{@post.id}" # PDFファイル名
      end
    end
  end
end

wicked_pdfが`ActionController::Base`の`render`メソッドをオーバーライドして、`render pdf:`の場合、HTMLをレンダリングしてからPDF化するようにします。

`app/views/posts/show.pdf.erb`のファイルにHTMLコードを書くと、ユーザーがアクセスしたときにPDFとして表示されます。

render pdf:以外にwicked_pdfrenderメソッドにはいろいろなオプションがあります。
こちらのページ
が参考になるでしょう。

PDFをファイルサーバーに保存する

実際には、PDF化されたファイルをAWSのS3などに保存する必要があります。
そのために、wicked_pdfが提供する`pdf_from_string`メソッドを使うサービスを作りました。

class HtmlToPdfService
  DEFAULT_LAYOUT = "pdf/layouts/application"

  attr_reader :template, :out_file, :data, :layout, :pdf_options

  def initialize template, out_file, data = {}, layout: DEFAULT_LAYOUT, pdf_options: {}
    @template = template
    @out_file = out_file
    @data = data # ファイルテンプレートに使われるローカル変数のハッシュ
    @layout = layout # 使われる共通レイアウト
    @pdf_options = pdf_options # ページサイズ・オリエンテーションなどのオプション
  end

  def perform
    html = ActionController::Base.render template: template, layout: layout, locals: data
    pdf = WickedPdf.new.pdf_from_string html, pdf_options
    File.write out_file, pdf, mode: "wb"
    true
  rescue StandardError => e
    Rails.logger.error e.message
    false
  end
end

レイアウトファイルはこんな感じです(ERBの代わりにSlimを使います)。

doctype html
html lang="ja"
  head
    meta charset="utf-8"
    = wicked_pdf_stylesheet_link_tag "pdf/application", media: "all"
    - if content_for? :stylesheets
      = yield :stylesheets
  body
    = yield
    = wicked_pdf_javascript_include_tag "pdf/application"    
    - if content_for? :javascripts
      = yield :javascripts

wkhtmltopdfはRailsサービス外で走るため、CSS/JSファイルの絶対URLを指定しなければいけません。Railsのstylesheet_link_tagjavascript_include_tagのhelpersメソッドを使えないので、wicked_pdf_stylesheet_link_tagwicked_pdf_javascript_include_tagを使いました。

次にテストのレイアウトを作成して

h1
  | The quick brown fox jumps over the lazy dog.

以下のようにサービスメソッドを実行すれば、

pdf_options = {
  page_size: "A4",
  margin: {
    top:    35, # mm
    bottom: 30, # mm
    left:   30, # mm
    right:  30  # mm
  }
}
HtmlToPdfService.new("pdf/test", "out.pdf", pdf_options: pdf_options).perform

こんなPDFファイルが生成されます。

カスタマイズフォントの利用

少しCSSを調整してみます。

@font-face {
  font-family: 'Caveat';
  src: font-url('Caveat.ttf');
}

h1 {
  font-size: 3rem;
  font-family: 'Caveat';
  color: green;
}

`Caveat.ttf`ファイルをapp/assets/fontsにおいておきました。

結果は真っ白のファイルでした!!!

解決方法は2つあります。

解決方法①:スタイルをCSSファイルに定義する場合

スタイルをCSSファイルに定義するには、`src: url(data:font/ttf;base64,AAEAAAATAQAABAAwR1BPU+Df..)`のようにbase64-encodedのデータにする必要があります。wicked_pdfではこのように書きます。

<% environment.context_class.instance_eval { include WickedPdf::WickedPdfHelper::Assets } %>

@font-face {
  font-family: 'Caveat';
  src: url('<%= wicked_pdf_asset_base64 "Caveat.ttf" %>');;
}

h1 {
  font-size: 3rem;
  font-family: 'Caveat';
  color: green;
}

解決方法②:スタイルをHTMLファイルに定義する場合

以下の2つの方法で解決できます。

- content_for :stylesheets do
  css:
    @font-face {
      font-family: 'Caveat';
      src: url('#{Rails.root.join "app", "assets", "fonts", "Caveat.ttf"}');
    }
- content_for :stylesheets do
  css:
    @font-face {
      font-family: 'Caveat';
      src: url('#{wicked_pdf_asset_base64 "Caveat.ttf"}');
    }

結果はこちら。

PDFファイルからPDFファイルを生成する

例として、以下の履歴書のPDFテンプレートに情報を記入して、新規のPDFファイルを生成したい場合の手順を説明します。

 Prawnというgemを使います。Rubyラッパーのwicked_pdfと違って、Rubyで書かれたライブラリです。たくさんのオプションがあり、RubyのPDF生成のgemの中では最も人気です。

インストールは、Gemfileに以下の行を入れてからbundle installを実行すれば終了です。

gem 'prawn'

 Prawnの使い方は、http://prawnpdf.org/manual.pdfを参考にしてください。

注意点は、 PrawnがPDF修正をサポートしていないことです。

まず追加内容のPDFファイルを作成して、

その後、2つのPDFファイルをマージすることで、以下のようになります。

PDF生成を簡略化するサービスを書いてみました。

class PdfToPdfService
  PAGE_MARGIN = [0, 0, 0, 0].freeze

  attr_reader :template_file, :template_pdf, :content_pdf, :out_file, :data

  def initialize template_file, out_file, data
    @template_file = template_file
    @template_pdf = CombinePDF.load template_file
    @content_pdf = Prawn::Document.new skip_page_creation: true
    @out_file = out_file
    @data = data # 各ページに書き込むデータ
  end

  def perform
    create_content_file
    merge_content_with_template
    true
  rescue StandardError => e
    Rails.logger.error e.message
    false
  ensure
    FileUtils.rm_f content_file
  end

  private
  def create_content_file
    template_pdf.pages.each_with_index do |template_page, page_idx|
      _, _, width, height = template_page.mediabox
      content_pdf.start_new_page size: [width, height], margin: PAGE_MARGIN
      add_content_page data[page_idx]
    end

    content_pdf.render_file content_file
  end

  def add_content_page page_data
    page_data.each do |input| 
      content_pdf.send input[0], *input[1..-1]
    end
  end

  def merge_content_with_template
    # merge từng page của file content với từng page của file template
    content_pages = CombinePDF.load(content_file).pages
    template_pdf.pages.each_with_index do |page, page_idx|
      page << content_pages[page_idx]
    end
    template_pdf.save out_file
  end

  def content_file
    @content_file ||= Rails.root.join "tmp", "content_#{Time.now.to_i}_#{SecureRandom.hex}.pdf"
  end
end

呼び出しはこんな感じです。

data = [
  [
    [:font, Rails.root.join("app", "assets", "fonts", "ipaex_mincho.ttf")], # set font cho toàn bộ content file
    [:text_box, "2018", at: [260, 790], size: 10], # điền text "2018" với font size 10 vào vị trí (260, 790)
    [:text_box, "6", at: [307, 790], size: 10], 
    [:text_box, "20", at: [333, 790], size: 10],
    [:text_box, "グエン ドゥック トゥン", at: [135, 774], size: 10],
    [:text_box, "NGUYEN DUC TUNG", at: [135, 748], size: 20],
    [:text_box, "1991", at: [120, 701], size: 10],
    [:text_box, "2", at: [170, 701], size: 10],
    [:text_box, "31", at: [210, 701], size: 10],
    [:text_box, "26", at: [285, 701], size: 10],
    [:stroke_ellipse, [348, 688], 10], # vẽ đường tròn với bán kính 10px ở vị trí (348, 688)
    [:text_box, "トウキョウト シンジュクク シンジュク ゴチョウメ ニノイチ", at: [135, 673], size: 10],
    [:text_box, "160-0022", at: [135, 658], size: 10],
    [:text_box, "東京都新宿区新宿5丁目2ー1", at: [135, 635], size: 18],
    [:text_box, "0987654321", at: [425, 653], size: 12]
  ],
  [
    # page 2 tạm thời không có nội dung nên truyền vào mảng rỗng
  ]
]

PdfToPdfService.new("CV.pdf", "out.pdf", data).perform

Wordファイル・ExcelファイルなどからPDF生成

オープンソースのLibreOfficeを使います。

  • Wordファイルから
/path/to/libreoffice --headless --convert-to pdf:writer_pdf_Export input.docx
  • Excelファイルから
/path/to/libreoffice --headless --convert-to pdf:calc_pdf_Export input.docx
  • PowerPointファイルから
/path/to/libreoffice --headless --convert-to pdf:impress_pdf_Export input.docx

ただ、MicrosoftOfficeのファイルだとレイアウトが壊れるケースが多いです。
LibreOfficeで開いて、レイアウトを調整してから行なったほうが良いでしょう。

※有料のライブラリを使った経験はありませんが、Googleで調べてみたら結構出ていました。10万円以上のものばかりです。

まとめ

  • ファイル内容がほぼテキストで、画像や図が少ない
    → Wicked PDFを使って、HTMLから作成
  • 決まったフォーマットで、空白の箇所にテキストを入れるだけ
    → Prawn PDFを利用
  • フォーマットがが決まらず、画像や図が多い
    → LibreOfficeライブラリを利用、Wordファイル・ExcelファイルなどをPDF化

ここまでお読みいただき、ありがとうございました。

Vibloの元記事:https://viblo.asia/p/cac-giai-phap-tao-file-pdf-trong-ruby-on-rails-3P0lPz8GKox