RailsのViewで作るPDF — Chromiumを活用した募集図面生成ロジック

はじめに

こんにちは、ITANDI株式会社でエンジニアをしている須田です。2023年に新卒で入社し、社会人・エンジニアともに3年目を迎えました。

普段は主に、Ruby on Railsを使用したバックエンド開発、およびNext.jsを使用したフロントエンド開発で、不動産管理会社様や仲介会社様が物件情報を管理するための「物件システム」の開発を担当しています。

先日、その開発業務の一環として「募集図面作成機能」をリリースしました。

本記事では、この機能で扱った「募集図面作成」という不動産業界特有の課題を、どのように解決したのか、その技術的なアプローチをご紹介します。

募集図面とは

今回ご紹介する機能で出力したITANDIフォーマットの募集図面

募集図面とは、お部屋探しをする際に目にする、家賃や間取り、設備といった物件情報がまとめられた資料のことです。デザイン性の高いこれらの図面ですが、実は多くの不動産会社の現場ではExcel等を使い、手作業で一枚ずつ作成されています。

物件情報をコピー&ペーストし、写真や間取り図を慎重に配置する…。

こうした地道な作業は1件あたり最低でも5分はかかります。さらに、家賃の入力ミスといった「情報の誤記リスク」も深刻な課題でした。

そこで私たちは、お客様にご登録いただいた物件情報を使い、募集図面のPDFを自動で出力する機能を開発し、これらの課題を解決しました。

RailsとChromiumで出力するPDF

PDF出力には、Rails標準のerbを使ってHTML・CSSでテンプレートデザインを作成し、それをPDFに変換するアプローチを採用しました。

このアプローチの最大のメリットは、Railsエンジニアが日頃から使い慣れたWeb開発の技術をそのまま活かせる点です。 これは、PDF生成のために新たなライブラリの記法や特殊なDSLを学ぶ必要がなく、チームの誰でも 学習コストゼロ で開発に着手できるということを意味します。

このアプローチを実現する上で重要な役割を果たすのが、オープンソースのWebブラウザ、Chromiumのヘッドレスモードです。通常、ブラウザを起動するとWebページを表示するための画面(GUI)が現れますが、ヘッドレスモードを使用するとGUIを表示せずにCLIでブラウザの機能を実行できるようになります。

これにより、Railsからブラウザを操作し、PDF生成を実行することが可能になります。

実装の全体像

ディレクトリ構造

今回のPDF出力はAPIとして実装しているため、コントローラーは app/controllers/api 配下に配置します。

PDFのデザインを定義するテンプレートにはRails標準のerbを使用するため、特別な場所を用意するのではなく、app/views配下に配置します。

app/
├── controllers/
│   └── api/
│       └── zumens_controller.rb
└── views/
    └── layouts/
        └── zumen.html.erb

コントローラーの全体像

PDFの生成から出力までを担うコントローラーのコードです。主に以下の3つのステップで処理が完結します。

class ZumensController < ApplicationController
  def new
    # 1. `render_to_string`によるHTML生成
    html_string = render_to_string(
      template: "layouts/zumen.html.erb",
      locals: { property: @property } # 事前に取得した物件情報
    )
    
    # 2. Chromiumを利用したPDF変換
    ##  Chromiumの実行コマンドは、ファイルパスを引数にとるので、HTML文字列を一時ファイルに書き出す
    input_html_path = create_tempfile(html_string)
    output_pdf_path = # 任意のファイル名を指定
    chromium_command = "chromium-browser --headless --print-to-pdf=#{output_pdf_path} #{input_html_path}"
    system(chromium_command, { exception: true })
    pdf_data = File.read(output_pdf_path)

    # 3. `send_data`によるPDF出力
    send_data(
      pdf_data,
      filename: "zumen.pdf",
      type: 'application/pdf'
    )
  ensure
    # 一時ファイルをクリーンアップ
  end
end

このように全体の流れは見通しが良く、非常にシンプルです。

PDFのテンプレートデザイン

PDFのテンプレートデザインは、Controllerから渡された物件情報を使いつつ、erbで作成します。

# app/views/layouts/zumen.html.erb
<div class="zumen">
  <div class="header">
  </div>

  <div class="body">
    <div class=”property-info”>
      <div><%= @property.name %></div>
      <div>賃料: <%= @property.chinryo %></div>
      <div>住所: <%= @property.address %></div>
    </div>
  </div>
  
  <div class="footer">
  </div>
</div>

<style>
  .property-info {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
</style>

Chromiumを使っているため、レイアウトにはdisplay: flexなどのモダンCSSを制約なく利用できます。これにより、複雑なデザインの図面であっても、Webページをコーディングするのと同じ感覚で、柔軟にスタイリングすることが可能です。

このおかげで、実際にデザイナーからの細かなレイアウト調整の要求にも、的確に応えることができました。

再掲(ITANDIで提供している募集図面のデザイン)

例えば上記図面の左上にあるオレンジ色の斜めのデザインのように、ITANDIならではのデザイン的特徴も、CSSによって忠実に表現しています。

各ステップの詳細

ここからは、このPDF出力の主要なステップを詳しく見ていきます。

1. render_to_stringによるHTML生成

まず、PDFのデザインの元となるHTMLを、render_to_stringというRailsのメソッドを使って組み立てます。これは、Webページを表示する renderメソッドとほぼ同じですが、HTMLをブラウザに送らず、ただの文字列として返してくれるメソッドです。

このメソッドに用意したerbテンプレートのパスと、あらかじめ取得した物件情報を渡すことで、動的にHTMLを生成します。

render_to_string(
  template: "layout/zumen.html.erb",
  locals: { property: @property } # あらかじめ取得した物件情報
)

ただ、render_to_stringAction PackAbstractController::Renderingモジュールで定義されているメソッドです。この機能は、RailsのActionController::Baseに自動で組み込まれるため、デフォルトでは ActionControllerでしか呼び出すことができません。

本来であれば、単体テストを書きやすくするためにロジックをコントローラーの外(例えばサービスクラスなど)へ切り出したいところです。しかし、前述の制約があるため、それが難しくなるという課題が存在します。

2. Chromiumを利用したPDF変換

次に、生成したHTML文字列をPDFに変換します。ここでは、Rubyのsystemメソッドを使って、Chromiumのコマンドを直接実行します。

ただし、ChromiumコマンドはHTMLの「ファイルパス」を引数に取るため、実行する前に、render_to_stringで生成したHTML文字列を一時ファイルに書き出しておく必要があります。

その上で、一時ファイルのパスを引数にしてChromiumのコマンドを実行します。

# HTML文字列を一時ファイルに書き出す
input_html_path = create_tempfile(html_string)
output_pdf_path = # 任意のファイル名を指定

# 実際に実行されるコマンドのイメージ
# ヘッドレスモードは`--headless`で指定
chromium_command = "chromium-browser --headless --print-to-pdf=#{output_pdf_path} #{input_html_path}"

system(chromium_command, { exception: true})

3. send_dataによるPDF出力

最後に、Chromiumが生成したPDFをバイナリデータとして読み込み、send_dataメソッドを使ってAPIのレスポンスとしてPDFを出力します。

send_data(
  pdf_data, # 生成したPDFデータ
  filename: "zumen.pdf",
  type: 'application/pdf'
)

この3つのステップで、PDFの生成から出力までを実現しています。

まとめ

この機能を開発したことで、これまで1件あたり5分以上かかっていた手作業を、わずか4秒程度で完了できるようになり、お客様の業務効率を劇的に改善できました。

その一方で、この「4秒」という時間はAPIのレスポンスとしてはまだ改善の余地があり、また、PDF生成時のメモリ使用率に起因するテストの不安定さといった、新たな技術的課題も見えています。

現在は賃貸部屋の募集図面にのみ対応していますが、今後はこの「HTMLテンプレートを差し替えるだけで様々なPDF出力に対応できる」という汎用的な仕組みを活かし、売買部屋など他の種別の図面作成にも展開していきたいと考えています。

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